Merge "tp: Stub of frames stdlib support" into main
diff --git a/Android.bp b/Android.bp
index 7f9dcb1..bd2825a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5151,6 +5151,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -5239,6 +5240,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -5310,6 +5312,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -6041,6 +6044,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -9177,6 +9181,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -9219,6 +9224,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.gen.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.gen.cc",
@@ -9261,6 +9267,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.gen.h",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.gen.h",
@@ -9299,6 +9306,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -9340,6 +9348,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -9381,6 +9390,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pb.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.pb.cc",
@@ -9422,6 +9432,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pb.h",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.pb.h",
@@ -9460,6 +9471,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -9502,6 +9514,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.pbzero.cc",
@@ -9544,6 +9557,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/counter_descriptor.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/debug_annotation.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/track_event/pixel_modem.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/screenshot.pbzero.h",
@@ -9712,6 +9726,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -11947,6 +11962,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
@@ -12371,8 +12387,10 @@
         "src/trace_processor/perfetto_sql/stdlib/prelude/casts.sql",
         "src/trace_processor/perfetto_sql/stdlib/prelude/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/prelude/trace_bounds.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/states.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span_with_slice.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_state_flattened.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql",
@@ -12806,6 +12824,8 @@
         "src/trace_redaction/process_thread_timeline.cc",
         "src/trace_redaction/proto_util.cc",
         "src/trace_redaction/prune_package_list.cc",
+        "src/trace_redaction/redact_sched_switch.cc",
+        "src/trace_redaction/redact_sched_waking.cc",
         "src/trace_redaction/scrub_ftrace_events.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_task_rename.cc",
@@ -12824,6 +12844,8 @@
         "src/trace_redaction/process_thread_timeline_unittest.cc",
         "src/trace_redaction/proto_util_unittest.cc",
         "src/trace_redaction/prune_package_list_unittest.cc",
+        "src/trace_redaction/redact_sched_switch_unittest.cc",
+        "src/trace_redaction/redact_sched_waking_unittest.cc",
         "src/trace_redaction/scrub_ftrace_events_unittest.cc",
         "src/trace_redaction/scrub_task_rename_unittest.cc",
         "src/trace_redaction/scrub_trace_packet_unittest.cc",
@@ -13872,6 +13894,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
@@ -15153,70 +15176,38 @@
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
-        ":perfetto_protos_perfetto_common_cpp_gen",
         ":perfetto_protos_perfetto_common_zero_gen",
-        ":perfetto_protos_perfetto_config_android_cpp_gen",
         ":perfetto_protos_perfetto_config_android_zero_gen",
-        ":perfetto_protos_perfetto_config_cpp_gen",
-        ":perfetto_protos_perfetto_config_ftrace_cpp_gen",
         ":perfetto_protos_perfetto_config_ftrace_zero_gen",
-        ":perfetto_protos_perfetto_config_gpu_cpp_gen",
         ":perfetto_protos_perfetto_config_gpu_zero_gen",
-        ":perfetto_protos_perfetto_config_inode_file_cpp_gen",
         ":perfetto_protos_perfetto_config_inode_file_zero_gen",
-        ":perfetto_protos_perfetto_config_interceptors_cpp_gen",
         ":perfetto_protos_perfetto_config_interceptors_zero_gen",
-        ":perfetto_protos_perfetto_config_power_cpp_gen",
         ":perfetto_protos_perfetto_config_power_zero_gen",
-        ":perfetto_protos_perfetto_config_process_stats_cpp_gen",
         ":perfetto_protos_perfetto_config_process_stats_zero_gen",
-        ":perfetto_protos_perfetto_config_profiling_cpp_gen",
         ":perfetto_protos_perfetto_config_profiling_zero_gen",
-        ":perfetto_protos_perfetto_config_statsd_cpp_gen",
         ":perfetto_protos_perfetto_config_statsd_zero_gen",
-        ":perfetto_protos_perfetto_config_sys_stats_cpp_gen",
         ":perfetto_protos_perfetto_config_sys_stats_zero_gen",
-        ":perfetto_protos_perfetto_config_system_info_cpp_gen",
         ":perfetto_protos_perfetto_config_system_info_zero_gen",
-        ":perfetto_protos_perfetto_config_track_event_cpp_gen",
         ":perfetto_protos_perfetto_config_track_event_zero_gen",
         ":perfetto_protos_perfetto_config_zero_gen",
-        ":perfetto_protos_perfetto_trace_android_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
-        ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
-        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
         ":perfetto_protos_perfetto_trace_etw_zero_gen",
-        ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
-        ":perfetto_protos_perfetto_trace_ftrace_cpp_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
-        ":perfetto_protos_perfetto_trace_gpu_cpp_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
-        ":perfetto_protos_perfetto_trace_interned_data_cpp_gen",
         ":perfetto_protos_perfetto_trace_interned_data_zero_gen",
-        ":perfetto_protos_perfetto_trace_minimal_cpp_gen",
         ":perfetto_protos_perfetto_trace_minimal_zero_gen",
-        ":perfetto_protos_perfetto_trace_non_minimal_cpp_gen",
         ":perfetto_protos_perfetto_trace_non_minimal_zero_gen",
-        ":perfetto_protos_perfetto_trace_perfetto_cpp_gen",
         ":perfetto_protos_perfetto_trace_perfetto_zero_gen",
-        ":perfetto_protos_perfetto_trace_power_cpp_gen",
         ":perfetto_protos_perfetto_trace_power_zero_gen",
         ":perfetto_protos_perfetto_trace_processor_zero_gen",
-        ":perfetto_protos_perfetto_trace_profiling_cpp_gen",
         ":perfetto_protos_perfetto_trace_profiling_zero_gen",
-        ":perfetto_protos_perfetto_trace_ps_cpp_gen",
         ":perfetto_protos_perfetto_trace_ps_zero_gen",
-        ":perfetto_protos_perfetto_trace_statsd_cpp_gen",
         ":perfetto_protos_perfetto_trace_statsd_zero_gen",
-        ":perfetto_protos_perfetto_trace_sys_stats_cpp_gen",
         ":perfetto_protos_perfetto_trace_sys_stats_zero_gen",
-        ":perfetto_protos_perfetto_trace_system_info_cpp_gen",
         ":perfetto_protos_perfetto_trace_system_info_zero_gen",
-        ":perfetto_protos_perfetto_trace_track_event_cpp_gen",
         ":perfetto_protos_perfetto_trace_track_event_zero_gen",
-        ":perfetto_protos_perfetto_trace_translation_cpp_gen",
         ":perfetto_protos_perfetto_trace_translation_zero_gen",
         ":perfetto_src_base_base",
         ":perfetto_src_protozero_protozero",
@@ -15259,70 +15250,38 @@
         "libz",
     ],
     generated_headers: [
-        "perfetto_protos_perfetto_common_cpp_gen_headers",
         "perfetto_protos_perfetto_common_zero_gen_headers",
-        "perfetto_protos_perfetto_config_android_cpp_gen_headers",
         "perfetto_protos_perfetto_config_android_zero_gen_headers",
-        "perfetto_protos_perfetto_config_cpp_gen_headers",
-        "perfetto_protos_perfetto_config_ftrace_cpp_gen_headers",
         "perfetto_protos_perfetto_config_ftrace_zero_gen_headers",
-        "perfetto_protos_perfetto_config_gpu_cpp_gen_headers",
         "perfetto_protos_perfetto_config_gpu_zero_gen_headers",
-        "perfetto_protos_perfetto_config_inode_file_cpp_gen_headers",
         "perfetto_protos_perfetto_config_inode_file_zero_gen_headers",
-        "perfetto_protos_perfetto_config_interceptors_cpp_gen_headers",
         "perfetto_protos_perfetto_config_interceptors_zero_gen_headers",
-        "perfetto_protos_perfetto_config_power_cpp_gen_headers",
         "perfetto_protos_perfetto_config_power_zero_gen_headers",
-        "perfetto_protos_perfetto_config_process_stats_cpp_gen_headers",
         "perfetto_protos_perfetto_config_process_stats_zero_gen_headers",
-        "perfetto_protos_perfetto_config_profiling_cpp_gen_headers",
         "perfetto_protos_perfetto_config_profiling_zero_gen_headers",
-        "perfetto_protos_perfetto_config_statsd_cpp_gen_headers",
         "perfetto_protos_perfetto_config_statsd_zero_gen_headers",
-        "perfetto_protos_perfetto_config_sys_stats_cpp_gen_headers",
         "perfetto_protos_perfetto_config_sys_stats_zero_gen_headers",
-        "perfetto_protos_perfetto_config_system_info_cpp_gen_headers",
         "perfetto_protos_perfetto_config_system_info_zero_gen_headers",
-        "perfetto_protos_perfetto_config_track_event_cpp_gen_headers",
         "perfetto_protos_perfetto_config_track_event_zero_gen_headers",
         "perfetto_protos_perfetto_config_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_android_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_gpu_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_interned_data_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_interned_data_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_minimal_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_minimal_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_non_minimal_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_non_minimal_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_perfetto_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_perfetto_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_power_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_power_zero_gen_headers",
         "perfetto_protos_perfetto_trace_processor_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_profiling_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_profiling_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_ps_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_ps_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_statsd_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_statsd_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_sys_stats_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_sys_stats_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_system_info_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_system_info_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_track_event_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_track_event_zero_gen_headers",
-        "perfetto_protos_perfetto_trace_translation_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_translation_zero_gen_headers",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
diff --git a/BUILD b/BUILD
index 3b56b52..efa579a 100644
--- a/BUILD
+++ b/BUILD
@@ -1950,6 +1950,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
@@ -2537,8 +2538,10 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_sched_sched",
     srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/states.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span_with_slice.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_state_flattened.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql",
@@ -4380,6 +4383,7 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
@@ -5232,6 +5236,7 @@
         "protos/perfetto/trace/track_event/counter_descriptor.proto",
         "protos/perfetto/trace/track_event/debug_annotation.proto",
         "protos/perfetto/trace/track_event/log_message.proto",
+        "protos/perfetto/trace/track_event/pixel_modem.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
         "protos/perfetto/trace/track_event/screenshot.proto",
diff --git a/gn/standalone/wasm.gni b/gn/standalone/wasm.gni
index 3b09531..eb6d4d4 100644
--- a/gn/standalone/wasm.gni
+++ b/gn/standalone/wasm.gni
@@ -48,6 +48,8 @@
       "-s",
       "INITIAL_MEMORY=33554432",
       "-s",
+      "MAXIMUM_MEMORY=4GB",
+      "-s",
       "ALLOW_MEMORY_GROWTH=1",
       "-s",
       "ALLOW_TABLE_GROWTH=1",
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 33f4195..5b8fab6 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -23,6 +23,7 @@
     "ad_services_metric.proto",
     "android_blocking_call.proto",
     "android_blocking_calls_cuj_metric.proto",
+    "android_blocking_calls_unagg.proto",
     "android_boot.proto",
     "android_boot_unagg.proto",
     "android_frame_timeline_metric.proto",
diff --git a/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto b/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
new file mode 100644
index 0000000..96b73ba
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/android_blocking_call.proto";
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// All blocking calls for a trace. Shows count and total duration for each.
+message AndroidBlockingCallsUnagg {
+
+    repeated ProcessWithBlockingCalls process_with_blocking_calls = 1;
+
+    message ProcessWithBlockingCalls {
+        // Details about the process (uid, version, etc)
+        optional AndroidProcessMetadata process = 1;
+
+        // List of blocking calls on the process main thread.
+        repeated AndroidBlockingCall blocking_calls = 2;
+    }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index f06f085..83395ec 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -29,6 +29,7 @@
 import "protos/perfetto/metrics/android/batt_metric.proto";
 import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
 import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
+import "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto";
 import "protos/perfetto/metrics/android/codec_metrics.proto";
 import "protos/perfetto/metrics/android/cpu_metric.proto";
 import "protos/perfetto/metrics/android/camera_metric.proto";
@@ -116,7 +117,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 64
+// Next id: 66
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -294,6 +295,9 @@
   // Specific for Android Auto
   optional AndroidMultiuserMetric android_auto_multiuser = 64;
 
+  // All blocking calls (e.g. binder calls) for a trace.
+  optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index fbd42fd..c2e2263 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -145,6 +145,24 @@
 
 // End of protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
+
+// All blocking calls for a trace. Shows count and total duration for each.
+message AndroidBlockingCallsUnagg {
+
+    repeated ProcessWithBlockingCalls process_with_blocking_calls = 1;
+
+    message ProcessWithBlockingCalls {
+        // Details about the process (uid, version, etc)
+        optional AndroidProcessMetadata process = 1;
+
+        // List of blocking calls on the process main thread.
+        repeated AndroidBlockingCall blocking_calls = 2;
+    }
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
+
 // Begin of protos/perfetto/metrics/android/android_boot.proto
 
 // This metric computes how much time processes spend in UNINTERRUPTIBLE_SLEEP
@@ -2520,7 +2538,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 64
+// Next id: 66
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -2698,6 +2716,9 @@
   // Specific for Android Auto
   optional AndroidMultiuserMetric android_auto_multiuser = 64;
 
+  // All blocking calls (e.g. binder calls) for a trace.
+  optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 0921dc0..74e49b6 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -12130,6 +12130,16 @@
 
 // End of protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto
 
+// Begin of protos/perfetto/trace/track_event/pixel_modem.proto
+
+// Event insights emitted by the Pixel modem.
+message PixelModemEventInsight {
+  // Opaque string containing arguments from the modem.
+  optional string detokenized_message = 1;
+}
+
+// End of protos/perfetto/trace/track_event/pixel_modem.proto
+
 // Begin of protos/perfetto/trace/track_event/screenshot.proto
 
 message Screenshot {
@@ -12215,7 +12225,7 @@
 // their default track association) can be emitted as part of a
 // TrackEventDefaults message.
 //
-// Next reserved id: 13 (up to 15). Next id: 51.
+// Next reserved id: 13 (up to 15). Next id: 52.
 message TrackEvent {
   // Names of categories of the event. In the client library, categories are a
   // way to turn groups of individual events on or off.
@@ -12369,6 +12379,7 @@
       43;
   optional ChromeActiveProcesses chrome_active_processes = 49;
   optional Screenshot screenshot = 50;
+  optional PixelModemEventInsight pixel_modem_event_insight = 51;
 
   // This field is used only if the source location represents the function that
   // executes during this event.
diff --git a/protos/perfetto/trace/track_event/BUILD.gn b/protos/perfetto/trace/track_event/BUILD.gn
index c101c46..00a5ff5 100644
--- a/protos/perfetto/trace/track_event/BUILD.gn
+++ b/protos/perfetto/trace/track_event/BUILD.gn
@@ -35,6 +35,7 @@
     "counter_descriptor.proto",
     "debug_annotation.proto",
     "log_message.proto",
+    "pixel_modem.proto",
     "process_descriptor.proto",
     "range_of_interest.proto",
     "screenshot.proto",
diff --git a/protos/perfetto/trace/track_event/pixel_modem.proto b/protos/perfetto/trace/track_event/pixel_modem.proto
new file mode 100644
index 0000000..ffe9062
--- /dev/null
+++ b/protos/perfetto/trace/track_event/pixel_modem.proto
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// Event insights emitted by the Pixel modem.
+message PixelModemEventInsight {
+  // Opaque string containing arguments from the modem.
+  optional string detokenized_message = 1;
+}
diff --git a/protos/perfetto/trace/track_event/track_event.proto b/protos/perfetto/trace/track_event/track_event.proto
index 325465d..2b9d35c 100644
--- a/protos/perfetto/trace/track_event/track_event.proto
+++ b/protos/perfetto/trace/track_event/track_event.proto
@@ -33,6 +33,7 @@
 import "protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto";
 import "protos/perfetto/trace/track_event/chrome_user_event.proto";
 import "protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto";
+import "protos/perfetto/trace/track_event/pixel_modem.proto";
 import "protos/perfetto/trace/track_event/screenshot.proto";
 import "protos/perfetto/trace/track_event/source_location.proto";
 
@@ -103,7 +104,7 @@
 // their default track association) can be emitted as part of a
 // TrackEventDefaults message.
 //
-// Next reserved id: 13 (up to 15). Next id: 51.
+// Next reserved id: 13 (up to 15). Next id: 52.
 message TrackEvent {
   // Names of categories of the event. In the client library, categories are a
   // way to turn groups of individual events on or off.
@@ -257,6 +258,7 @@
       43;
   optional ChromeActiveProcesses chrome_active_processes = 49;
   optional Screenshot screenshot = 50;
+  optional PixelModemEventInsight pixel_modem_event_insight = 51;
 
   // This field is used only if the source location represents the function that
   // executes during this event.
diff --git a/python/BUILD b/python/BUILD
index 3b65af7..52a511d 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -126,6 +126,9 @@
            ] + PERFETTO_CONFIG.deps.pandas_py +
            PERFETTO_CONFIG.deps.protobuf_py +
            PERFETTO_CONFIG.deps.tp_vendor_py,
+    tags = [
+        "avoid_dep",
+    ],
 )
 
 # GN target: //python:trace_uri_resolver
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 6b4b352..a5fe326 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 6e1e1bb..bc832bf 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -68,6 +68,7 @@
 #include "protos/perfetto/trace/ftrace/mm_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/net.pbzero.h"
 #include "protos/perfetto/trace/ftrace/oom.pbzero.h"
+#include "protos/perfetto/trace/ftrace/panel.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h"
 #include "protos/perfetto/trace/ftrace/rpm.pbzero.h"
@@ -1143,6 +1144,10 @@
         ParseRpmStatus(ts, fld_bytes);
         break;
       }
+      case FtraceEvent::kPanelWriteGenericFieldNumber: {
+        ParsePanelWriteGeneric(ts, pid, fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -3329,6 +3334,22 @@
   devices_with_active_rpm_slice_.insert(device_name);
 }
 
+void FtraceParser::ParsePanelWriteGeneric(int64_t timestamp,
+                                          uint32_t pid,
+                                          ConstBytes blob) {
+  protos::pbzero::PanelWriteGenericFtraceEvent::Decoder evt(blob.data,
+                                                            blob.size);
+  if (!evt.type()) {
+    context_->storage->IncrementStats(stats::systrace_parse_failure);
+    return;
+  }
+
+  uint32_t tgid = static_cast<uint32_t>(evt.pid());
+  SystraceParser::GetOrCreate(context_)->ParseKernelTracingMarkWrite(
+      timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
+      evt.name(), tgid, evt.value());
+}
+
 StringId FtraceParser::InternedKernelSymbolOrFallback(
     uint64_t key,
     PacketSequenceStateGeneration* seq_state) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index a3f8fc6..402ad73 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -296,6 +296,9 @@
                                    protozero::ConstBytes);
   StringId GetRpmStatusStringId(int32_t rpm_status_val);
   void ParseRpmStatus(int64_t ts, protozero::ConstBytes);
+  void ParsePanelWriteGeneric(int64_t timestamp,
+                              uint32_t pid,
+                              protozero::ConstBytes);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index b06bb71..e00322b 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -98,11 +98,12 @@
   point.int_value = value;
   point.tgid = tgid;
 
-  // Some versions of this trace point fill trace_type with one of (B/E/C),
+  // Some versions of this trace point fill trace_type with one of (B/E/C/I),
   // others use the trace_begin boolean and only support begin/end events:
   if (trace_type == 0) {
     point.phase = trace_begin ? 'B' : 'E';
-  } else if (trace_type == 'B' || trace_type == 'E' || trace_type == 'C') {
+  } else if (trace_type == 'B' || trace_type == 'E' || trace_type == 'C' ||
+             trace_type == 'I') {
     point.phase = trace_type;
   } else {
     context_->storage->IncrementStats(stats::systrace_parse_failure);
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index f87b560..bd77d0a 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -25,6 +25,7 @@
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
+    "android_blocking_calls_unagg.sql",
     "android_boot.sql",
     "android_boot_unagg.sql",
     "android_camera.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
index eaa2104..5838125 100644
--- a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql
@@ -21,6 +21,7 @@
 
 INCLUDE PERFETTO MODULE android.slices;
 INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
 
 -- Jank "J<*>" and latency "L<*>" cujs are put together in android_cujs table.
 -- They are computed separately as latency ones are slightly different, don't
@@ -75,99 +76,22 @@
 SELECT ROW_NUMBER() OVER (ORDER BY ts) AS cuj_id, *
 FROM all_cujs;
 
-DROP TABLE IF EXISTS relevant_binder_calls_with_names;
-CREATE TABLE relevant_binder_calls_with_names AS
-SELECT DISTINCT
-    tx.aidl_name AS name,
-    tx.client_ts AS ts,
-    s.track_id,
-    tx.client_dur AS dur,
-    s.id,
-    tx.client_process as process_name,
-    tx.client_utid as utid,
-    tx.client_upid as upid
-FROM android_binder_txns AS tx
-         JOIN slice AS s ON s.id = tx.binder_txn_id
-WHERE is_main_thread AND aidl_name IS NOT NULL AND is_sync = 1;
-
-DROP TABLE IF EXISTS android_blocking_calls_cuj_calls;
-CREATE TABLE android_blocking_calls_cuj_calls AS
-WITH all_main_thread_relevant_slices AS (
-    SELECT DISTINCT
-        android_standardize_slice_name(s.name) AS name,
-        s.ts,
-        s.track_id,
-        s.dur,
-        s.id,
-        process.name AS process_name,
-        thread.utid,
-        process.upid
-    FROM slice s
-        JOIN thread_track ON s.track_id = thread_track.id
-        JOIN thread USING (utid)
-        JOIN process USING (upid)
-    WHERE
-        thread.is_main_thread AND (
-               s.name = 'measure'
-            OR s.name = 'layout'
-            OR s.name = 'configChanged'
-            OR s.name = 'animation'
-            OR s.name = 'input'
-            OR s.name = 'traversal'
-            OR s.name = 'Contending for pthread mutex'
-            OR s.name = 'postAndWait'
-            OR s.name GLOB 'monitor contention with*'
-            OR s.name GLOB 'SuspendThreadByThreadId*'
-            OR s.name GLOB 'LoadApkAssetsFd*'
-            OR s.name GLOB '*binder transaction*'
-            OR s.name GLOB 'inflate*'
-            OR s.name GLOB 'Lock contention on*'
-            OR s.name GLOB 'android.os.Handler: kotlinx.coroutines*'
-            OR s.name GLOB 'relayoutWindow*'
-            OR s.name GLOB 'ImageDecoder#decode*'
-            OR s.name GLOB 'NotificationStackScrollLayout#onMeasure'
-            OR s.name GLOB 'ExpNotRow#*'
-            OR s.name GLOB 'GC: Wait For*'
-            OR (
-                -- Some top level handler slices
-                    s.depth = 0
-                AND s.name NOT GLOB '*Choreographer*'
-                AND s.name NOT GLOB '*Input*'
-                AND s.name NOT GLOB '*input*'
-                AND s.name NOT GLOB 'android.os.Handler: #*'
-                AND (
-                   -- Handler pattern heuristics
-                      s.name GLOB '*Handler: *$*'
-                   OR s.name GLOB '*.*.*: *$*'
-                   OR s.name GLOB '*.*$*: #*'
-                )
-            )
-        )
-    UNION ALL
-    SELECT
-        name,
-        ts,
-        track_id,
-        dur,
-        id,
-        process_name,
-        utid,
-        upid
-    FROM relevant_binder_calls_with_names
-),
--- Now we have:
---  (1) a list of slices from the main thread of each process
+-- We have:
+--  (1) a list of slices from the main thread of each process from the
+--  all_main_thread_relevant_slices table.
 --  (2) a list of android cuj with beginning, end, and process
 -- It's needed to:
 --  (1) assign a cuj to each slice. If there are multiple cujs going on during a
 --      slice, there needs to be 2 entries for that slice, one for each cuj id.
 --  (2) each slice needs to be trimmed to be fully inside the cuj associated
 --      (as we don't care about what's outside cujs)
+DROP TABLE IF EXISTS android_blocking_calls_cuj_calls;
+CREATE TABLE android_blocking_calls_cuj_calls AS
+WITH
 main_thread_slices_scoped_to_cujs AS (
 SELECT
     s.id,
     s.id AS slice_id,
-    s.track_id,
     s.name,
     max(s.ts, cuj.ts) AS ts,
     min(s.ts + s.dur, cuj.ts_end) as ts_end,
@@ -177,7 +101,7 @@
     s.process_name,
     s.upid,
     s.utid
-FROM all_main_thread_relevant_slices s
+FROM _android_critical_blocking_calls s
     JOIN  android_cujs cuj
     -- only when there is an overlap
     ON s.ts + s.dur > cuj.ts AND s.ts < cuj.ts_end
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql
new file mode 100644
index 0000000..2008c0b
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql
@@ -0,0 +1,96 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('android/process_metadata.sql');
+
+INCLUDE PERFETTO MODULE android.slices;
+INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
+
+DROP TABLE IF EXISTS process_info;
+CREATE TABLE process_info AS
+SELECT
+  process.upid AS upid,
+  process.name AS process_name,
+  process_metadata.metadata AS process_metadata
+FROM process
+JOIN process_metadata USING (upid);
+
+DROP TABLE IF EXISTS android_blocking_calls_unagg_calls;
+CREATE TABLE android_blocking_calls_unagg_calls AS
+SELECT
+  name,
+  COUNT(*) AS occurrences,
+  MAX(dur) AS max_dur_ns,
+  MIN(dur) AS min_dur_ns,
+  SUM(dur) AS total_dur_ns,
+  upid,
+  process_name
+FROM
+  _android_critical_blocking_calls
+GROUP BY name, upid, process_name;
+
+DROP TABLE IF EXISTS filtered_processes_with_non_zero_blocking_calls;
+CREATE TABLE filtered_processes_with_non_zero_blocking_calls AS
+SELECT pi.upid,
+  pi.process_name,
+  pi.process_metadata
+FROM process_info pi WHERE pi.upid IN
+  (SELECT DISTINCT upid FROM _android_critical_blocking_calls);
+
+
+DROP TABLE IF EXISTS filtered_processes_with_non_zero_blocking_calls;
+CREATE TABLE filtered_processes_with_non_zero_blocking_calls AS
+SELECT pi.upid,
+  pi.process_name,
+  pi.process_metadata
+FROM process_info pi WHERE pi.upid IN
+  (SELECT DISTINCT upid FROM _android_critical_blocking_calls);
+
+DROP VIEW IF EXISTS android_blocking_calls_unagg_output;
+CREATE PERFETTO VIEW android_blocking_calls_unagg_output AS
+SELECT AndroidBlockingCallsUnagg(
+  'process_with_blocking_calls', (
+     SELECT RepeatedField(
+       AndroidBlockingCallsUnagg_ProcessWithBlockingCalls(
+         'process', e.process_metadata,
+         'blocking_calls', (
+            SELECT RepeatedField(
+              AndroidBlockingCall(
+                'name', d.name,
+                'cnt', d.occurrences,
+                'total_dur_ms', CAST(total_dur_ns / 1e6 AS INT),
+                'max_dur_ms', CAST(max_dur_ns / 1e6 AS INT),
+                'min_dur_ms', CAST(min_dur_ns / 1e6 AS INT),
+                'total_dur_ns', d.total_dur_ns,
+                'max_dur_ns', d.max_dur_ns,
+                'min_dur_ns', d.min_dur_ns
+              )
+            ) FROM (
+            SELECT b.name,
+              b.occurrences,
+              b.total_dur_ns,
+              b.max_dur_ns,
+              b.min_dur_ns
+            FROM android_blocking_calls_unagg_calls b INNER JOIN filtered_processes_with_non_zero_blocking_calls c
+            ON b.upid = c.upid WHERE b.upid = e.upid
+            ORDER BY total_dur_ns DESC
+            ) d
+         )
+       )
+     )
+     FROM filtered_processes_with_non_zero_blocking_calls e
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/android_startup.sql b/src/trace_processor/metrics/sql/android/android_startup.sql
index 18f9fbf..4564f64 100644
--- a/src/trace_processor/metrics/sql/android/android_startup.sql
+++ b/src/trace_processor/metrics/sql/android/android_startup.sql
@@ -504,16 +504,6 @@
   ) AS startup
 FROM android_startups launches;
 
-DROP VIEW IF EXISTS android_startup_event;
-CREATE PERFETTO VIEW android_startup_event AS
-SELECT
-  'slice' AS track_type,
-  'Android App Startups' AS track_name,
-  l.ts AS ts,
-  l.dur AS dur,
-  l.package AS slice_name
-FROM android_startups l;
-
 DROP VIEW IF EXISTS android_startup_output;
 CREATE PERFETTO VIEW android_startup_output AS
 SELECT
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
index 383ed17..59e571f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
@@ -38,6 +38,7 @@
 #include "protos/perfetto/trace/ftrace/g2d.pbzero.h"
 #include "protos/perfetto/trace/ftrace/irq.pbzero.h"
 #include "protos/perfetto/trace/ftrace/mdss.pbzero.h"
+#include "protos/perfetto/trace/ftrace/panel.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "protos/perfetto/trace/ftrace/samsung.pbzero.h"
 #include "protos/perfetto/trace/ftrace/sched.pbzero.h"
@@ -459,6 +460,19 @@
     writer_->AppendString("|");
     WriteValueForField(TMW::kValueFieldNumber, DVW());
     return;
+  } else if (event_name_ == "panel_write_generic") {
+    using TMW = protos::pbzero::PanelWriteGenericFtraceEvent;
+    WriteValueForField(TMW::kTypeFieldNumber, [this](const Variadic& value) {
+      PERFETTO_DCHECK(value.type == Variadic::Type::kUint);
+      writer_->AppendChar(static_cast<char>(value.uint_value));
+    });
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kPidFieldNumber, DVW());
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kNameFieldNumber, DVW());
+    writer_->AppendString("|");
+    WriteValueForField(TMW::kValueFieldNumber, DVW());
+    return;
   } else if (event_name_ == "g2d_tracing_mark_write") {
     using TMW = protos::pbzero::G2dTracingMarkWriteFtraceEvent;
     WriteValueForField(TMW::kTypeFieldNumber, [this](const Variadic& value) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc
index 4ff33cc..dda42ab 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc
@@ -86,20 +86,20 @@
   std::vector<Edge> roots;
   bool parse_error = false;
   auto root_node_ids = start.int_values(&parse_error);
-  auto max_weights = end.int_values(&parse_error);
+  auto target_weights = end.int_values(&parse_error);
 
-  for (; root_node_ids && max_weights; ++root_node_ids, ++max_weights) {
+  for (; root_node_ids && target_weights; ++root_node_ids, ++target_weights) {
     roots.push_back(Edge{static_cast<uint32_t>(*root_node_ids),
-                         static_cast<uint32_t>(*max_weights)});
+                         static_cast<uint32_t>(*target_weights)});
   }
 
   if (parse_error) {
     return base::ErrStatus(
-        "Failed while parsing root_node_ids or root_max_weights");
+        "Failed while parsing root_node_ids or root_target_weights");
   }
-  if (static_cast<bool>(root_node_ids) != static_cast<bool>(max_weights)) {
+  if (static_cast<bool>(root_node_ids) != static_cast<bool>(target_weights)) {
     return base::ErrStatus(
-        "dfs_weight_bounded: length of root_node_ids and root_max_weights "
+        "dfs_weight_bounded: length of root_node_ids and root_target_weights "
         "columns is not the same");
   }
   return roots;
@@ -108,7 +108,8 @@
 void DfsWeightBoundedImpl(
     tables::DfsWeightBoundedTable* table,
     const std::vector<Destinations>& source_to_destinations_map,
-    const std::vector<Edge>& roots) {
+    const std::vector<Edge>& roots,
+    const bool is_target_weight_floor) {
   struct StackState {
     uint32_t id;
     uint32_t weight;
@@ -131,22 +132,26 @@
         continue;
       }
       seen_node_ids[stack_state.id] = true;
-
-      // We want to greedily return all possible edges that are reachable within
-      // the target weight. If an edge already fails the requirement, skip it
-      // and don't include it's weight but continue the search, some other edges
-      // might fit.
-      if (total_weight + stack_state.weight > root.weight) {
-        continue;
-      }
       total_weight += stack_state.weight;
 
+      if (!is_target_weight_floor && total_weight > root.weight) {
+        // If target weight is a ceiling weight then we don't want to include
+        // the last node that crosses the threshold.
+        break;
+      }
+
       tables::DfsWeightBoundedTable::Row row;
       row.root_node_id = root.id;
       row.node_id = stack_state.id;
       row.parent_node_id = stack_state.parent_id;
       table->Insert(row);
 
+      if (total_weight > root.weight) {
+        // If the target weight is a floor weight, we add the last node that
+        // crossed the threshold before exiting the search.
+        break;
+      }
+
       PERFETTO_DCHECK(stack_state.id < source_to_destinations_map.size());
 
       const auto& children = source_to_destinations_map[stack_state.id];
@@ -176,24 +181,29 @@
 
 base::StatusOr<std::unique_ptr<Table>> DfsWeightBounded::ComputeTable(
     const std::vector<SqlValue>& arguments) {
-  PERFETTO_CHECK(arguments.size() == 5);
+  PERFETTO_CHECK(arguments.size() == 6);
 
   const SqlValue& raw_source_ids = arguments[0];
   const SqlValue& raw_dest_ids = arguments[1];
   const SqlValue& raw_edge_weights = arguments[2];
   const SqlValue& raw_root_ids = arguments[3];
-  const SqlValue& raw_root_max_weights = arguments[4];
+  const SqlValue& raw_root_target_weights = arguments[4];
+  const SqlValue& raw_is_target_weight_floor = arguments[5];
 
   if (raw_source_ids.is_null() && raw_dest_ids.is_null() &&
-      raw_edge_weights.is_null() && raw_root_ids.is_null() &&
-      raw_root_max_weights.is_null()) {
+      raw_edge_weights.is_null()) {
+    return std::unique_ptr<Table>(
+        std::make_unique<tables::DfsWeightBoundedTable>(pool_));
+  }
+
+  if (raw_root_ids.is_null() && raw_root_target_weights.is_null()) {
     return std::unique_ptr<Table>(
         std::make_unique<tables::DfsWeightBoundedTable>(pool_));
   }
 
   if (raw_source_ids.is_null() || raw_dest_ids.is_null() ||
       raw_edge_weights.is_null() || raw_root_ids.is_null() ||
-      raw_root_max_weights.is_null()) {
+      raw_root_target_weights.is_null()) {
     return base::ErrStatus(
         "dfs_weight_bounded: either all arguments should be null or none "
         "should be");
@@ -214,9 +224,9 @@
     return base::ErrStatus(
         "dfs_weight_bounded: root_ids should be a repeated field");
   }
-  if (raw_root_max_weights.type != SqlValue::kBytes) {
+  if (raw_root_target_weights.type != SqlValue::kBytes) {
     return base::ErrStatus(
-        "dfs_weight_bounded: root_max_weights should be a repeated field");
+        "dfs_weight_bounded: root_target_weights should be a repeated field");
   }
 
   protos::pbzero::ProtoBuilderResult::Decoder proto_source_ids(
@@ -263,25 +273,27 @@
   protos::pbzero::RepeatedBuilderResult::Decoder root_ids(
       proto_root_ids.repeated());
 
-  protos::pbzero::ProtoBuilderResult::Decoder proto_root_max_weights(
-      static_cast<const uint8_t*>(raw_root_max_weights.AsBytes()),
-      raw_root_max_weights.bytes_count);
-  if (!proto_root_max_weights.is_repeated()) {
+  protos::pbzero::ProtoBuilderResult::Decoder proto_root_target_weights(
+      static_cast<const uint8_t*>(raw_root_target_weights.AsBytes()),
+      raw_root_target_weights.bytes_count);
+  if (!proto_root_target_weights.is_repeated()) {
     return base::ErrStatus(
-        "dfs_weight_bounded: root_max_weights is not generated by "
+        "dfs_weight_bounded: root_target_weights is not generated by "
         "RepeatedField function");
   }
-  protos::pbzero::RepeatedBuilderResult::Decoder root_max_weights(
-      proto_root_max_weights.repeated());
+  protos::pbzero::RepeatedBuilderResult::Decoder root_target_weights(
+      proto_root_target_weights.repeated());
 
+  bool is_target_weight_floor =
+      static_cast<bool>(raw_is_target_weight_floor.AsLong());
   ASSIGN_OR_RETURN(auto map, ParseSourceToDestionationsMap(source_ids, dest_ids,
                                                            edge_weights));
 
   ASSIGN_OR_RETURN(auto roots,
-                   ParseRootToMaxWeightMap(root_ids, root_max_weights));
+                   ParseRootToMaxWeightMap(root_ids, root_target_weights));
 
   auto table = std::make_unique<tables::DfsWeightBoundedTable>(pool_);
-  DfsWeightBoundedImpl(table.get(), map, roots);
+  DfsWeightBoundedImpl(table.get(), map, roots, is_target_weight_floor);
   return std::unique_ptr<Table>(std::move(table));
 }
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h
index f1af772..92caf25 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h
@@ -54,6 +54,13 @@
 //     uint32 values corresponding to the max sum of edge weights inclusive,
 //     at which point the DFS from the |root_node_ids| stops. This number of
 //     values should be the same as |root_node_ids|.
+//  6) |is_target_weight_floor|: Whether the target_weight is a floor weight or
+//  ceiling weight.
+//     If it's floor, the search stops right after we exceed the target weight,
+//     and we include the node that pushed just passed the target. If ceiling,
+//     the search stops right before the target weight and the node that would
+//     have pushed us passed the target is not included.
+
 //
 // Returns:
 //  A table with the nodes reachable from the start node, their "parent" in
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
index 0570082..e129565 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -206,6 +206,9 @@
         C("in_root_max_weights",
           CppOptional(CppUint32()),
           flags=ColumnFlag.HIDDEN),
+        C("in_is_target_weight_floor",
+          CppOptional(CppUint32()),
+          flags=ColumnFlag.HIDDEN),
     ])
 
 # Keep this list sorted.
diff --git a/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql b/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
index 6214716..34fc1de 100644
--- a/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/graphs/search.sql
@@ -131,7 +131,7 @@
 --      (
 --        SELECT
 --          id AS root_node_id,
---          id - COALESCE(prev_id, id) AS root_max_weight
+--          id - COALESCE(prev_id, id) AS root_target_weight
 --        FROM _wakeup_chain
 --      ));
 -- ```
@@ -150,7 +150,7 @@
   graph_table TableOrSubquery,
   -- A table/view/subquery corresponding to start nodes to |graph_table| which will be the
   -- roots of the reachability trees. This table must have the columns
-  -- "root_node_id" and "root_max_weight" corresponding to the starting node id and the max
+  -- "root_node_id" and "root_target_weight" corresponding to the starting node id and the max
   -- weight allowed on the tree.
   --
   -- Note: the columns must contain uint32 similar to ids in trace processor
@@ -158,7 +158,14 @@
   -- implementation makes assumptions on this for performance reasons and, if
   -- this criteria is not, can lead to enormous amounts of memory being
   -- allocated.
-  root_table TableOrSubquery
+  root_table TableOrSubquery,
+  -- Whether the target_weight is a floor weight or ceiling weight.
+  -- If it's floor, the search stops right after we exceed the target weight, and we
+  -- include the node that pushed just passed the target. If ceiling, the search stops
+  -- right before the target weight and the node that would have pushed us passed the
+  -- target is not included.
+  is_target_weight_floor Expr
+
 )
 -- The returned table has the schema (root_node_id, node_id UINT32, parent_node_id UINT32).
 -- |root_node_id| is the id of the starting node under which this edge was encountered.
@@ -175,6 +182,7 @@
     (SELECT RepeatedField(dest_node_id) FROM __temp_graph_table),
     (SELECT RepeatedField(edge_weight) FROM __temp_graph_table),
     (SELECT RepeatedField(root_node_id) FROM __temp_root_table),
-    (SELECT RepeatedField(root_max_weight) FROM __temp_root_table)
+    (SELECT RepeatedField(root_target_weight) FROM __temp_root_table),
+    $is_target_weight_floor
   ) dt
 );
diff --git a/src/trace_processor/perfetto_sql/stdlib/memory/heap_graph_dominator_tree.sql b/src/trace_processor/perfetto_sql/stdlib/memory/heap_graph_dominator_tree.sql
index 2326aab..28edf7f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/memory/heap_graph_dominator_tree.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/memory/heap_graph_dominator_tree.sql
@@ -160,4 +160,5 @@
   _subtree_native_size_bytes(t.id) AS dominated_native_size_bytes,
   d.depth
 FROM _heap_graph_dominator_tree t
-JOIN _heap_graph_dominator_tree_depth d USING(id);
+JOIN _heap_graph_dominator_tree_depth d USING(id)
+ORDER BY id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
index bd2dc94..9168926 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
@@ -17,8 +17,10 @@
 perfetto_sql_source_set("sched") {
   deps = [ "utilization" ]
   sources = [
+    "runnable.sql",
     "states.sql",
     "thread_executing_span.sql",
+    "thread_executing_span_with_slice.sql",
     "thread_level_parallelism.sql",
     "thread_state_flattened.sql",
     "time_in_state.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql b/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql
new file mode 100644
index 0000000..38956b1
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql
@@ -0,0 +1,53 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+-- Previous runnable slice on the same thread.
+-- For each "Running" thread state finds:
+-- - previous "Runnable" (or runnable preempted) state.
+-- - previous uninterrupted "Runnable" state with a valid waker thread.
+CREATE PERFETTO TABLE sched_previous_runnable_on_thread(
+    -- `thread_state.id` id.
+    id INT,
+    -- Previous runnable `thread_state.id`.
+    prev_runnable_id INT,
+    -- Previous runnable `thread_state.id` with valid waker
+    -- thread.
+    prev_wakeup_runnable_id INT
+) AS
+WITH running_and_runnable AS (
+  SELECT
+        id,
+        state,
+        MAX(id)
+          FILTER (WHERE state != 'Running')
+          OVER utid_part AS prev_runnable_id,
+        MAX(id)
+          FILTER (WHERE
+            waker_utid IS NOT NULL
+            AND (irq_context IS NULL OR irq_context != 1))
+          OVER utid_part AS prev_wakeup_runnable_id
+    FROM thread_state
+    -- Optimal operation for state IN (R, R+, Running)
+    WHERE state GLOB 'R*' AND dur != -1
+    WINDOW utid_part AS (PARTITION BY utid ORDER BY id)
+)
+SELECT
+  id,
+  prev_runnable_id,
+  prev_wakeup_runnable_id
+FROM running_and_runnable
+WHERE state = 'Running'
+ORDER BY id;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
index 348d261..e3a92e2 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql
@@ -14,7 +14,7 @@
 -- limitations under the License.
 --
 
-INCLUDE PERFETTO MODULE slices.flat_slices;
+INCLUDE PERFETTO MODULE graphs.search;
 
 -- A 'thread_executing_span' is thread_state span starting with a runnable slice
 -- until the next runnable slice that's woken up by a process (as opposed
@@ -33,7 +33,7 @@
 -- so this table might contain wakeups from interrupt context, consequently, the
 -- wakeup graph generated might not be accurate.
 --
-CREATE PERFETTO VIEW _runnable_state
+CREATE PERFETTO TABLE _runnable_state
 AS
 SELECT
   thread_state.id,
@@ -50,7 +50,7 @@
   AND (thread_state.irq_context = 0 OR thread_state.irq_context IS NULL);
 
 -- Similar to |_runnable_state| but finds the first runnable state at thread.
-CREATE PERFETTO VIEW _first_runnable_state
+CREATE PERFETTO TABLE _first_runnable_state
 AS
 WITH
   first_state AS (
@@ -77,7 +77,7 @@
 
 --
 -- Finds all sleep states including interruptible (S) and uninterruptible (D).
-CREATE PERFETTO VIEW _sleep_state
+CREATE PERFETTO TABLE _sleep_state
 AS
 SELECT
   thread_state.id,
@@ -92,7 +92,7 @@
 --
 -- Finds the last execution for every thread to end executing_spans without a Sleep.
 --
-CREATE PERFETTO VIEW _thread_end_ts
+CREATE PERFETTO TABLE _thread_end_ts
 AS
 SELECT
   MAX(ts) + dur AS end_ts,
@@ -102,7 +102,7 @@
 GROUP BY utid;
 
 -- Similar to |_sleep_state| but finds the first sleep state in a thread.
-CREATE PERFETTO VIEW _first_sleep_state
+CREATE PERFETTO TABLE _first_sleep_state
 AS
 SELECT
   MIN(s.id) AS id,
@@ -145,42 +145,45 @@
 -- end_ts           = S1_ts.
 CREATE PERFETTO TABLE _wakeup
 AS
+WITH
+  all_wakeups AS (
+    SELECT
+      s.state,
+      s.blocked_function,
+      r.id,
+      r.ts AS ts,
+      r.utid AS utid,
+      r.waker_id,
+      r.waker_utid,
+      s.ts AS prev_end_ts
+    FROM _runnable_state r
+    JOIN _sleep_state s
+      ON s.utid = r.utid AND (s.ts + s.dur = r.ts)
+    UNION ALL
+    SELECT
+      NULL AS state,
+      NULL AS blocked_function,
+      r.id,
+      r.ts,
+      r.utid AS utid,
+      r.waker_id,
+      r.waker_utid,
+      NULL AS prev_end_ts
+    FROM _first_runnable_state r
+    LEFT JOIN _first_sleep_state s
+      ON s.utid = r.utid
+  )
 SELECT
-  s.state,
-  s.blocked_function,
-  r.id,
-  r.ts AS ts,
-  r.utid AS utid,
-  r.waker_id,
-  r.waker_utid,
-  IFNULL(LEAD(s.ts) OVER (PARTITION BY r.utid ORDER BY r.ts), thread_end.end_ts) AS end_ts,
-  s.ts AS prev_end_ts,
-  LAG(r.id) OVER (PARTITION BY r.utid ORDER BY r.ts) AS prev_id
-FROM _runnable_state r
-JOIN _sleep_state s
-  ON s.utid = r.utid AND (s.ts + s.dur = r.ts)
-LEFT JOIN _thread_end_ts thread_end
-  USING (utid)
-UNION ALL
-SELECT
-  NULL AS state,
-  NULL AS blocked_function,
-  r.id,
-  r.ts,
-  r.utid AS utid,
-  r.waker_id,
-  r.waker_utid,
-  IFNULL(s.ts, thread_end.end_ts) AS end_ts,
-  NULL AS prev_end_ts,
-  NULL AS prev_id
-FROM _first_runnable_state r
-LEFT JOIN _first_sleep_state s
-  ON s.utid = r.utid
+  all_wakeups.*,
+  LAG(id) OVER (PARTITION BY utid ORDER BY ts) AS prev_id,
+  IFNULL(LEAD(prev_end_ts) OVER (PARTITION BY utid ORDER BY ts), thread_end.end_ts) AS end_ts
+FROM all_wakeups
 LEFT JOIN _thread_end_ts thread_end
   USING (utid);
 
 -- Mapping from running thread state to runnable
-CREATE PERFETTO TABLE _waker_map
+-- TODO(zezeozue): Switch to use `sched_previous_runnable_on_thread`.
+CREATE PERFETTO TABLE _wakeup_map
 AS
 WITH x AS (
 SELECT id, waker_id, utid, state FROM thread_state WHERE state = 'Running' AND dur != -1
@@ -192,12 +195,12 @@
     SELECT
       id AS waker_id,
       state,
-      max(id)
+      MAX(id)
         filter(WHERE state = 'R')
           OVER (PARTITION BY utid ORDER BY id) AS id
     FROM x
   )
-SELECT id, waker_id FROM y WHERE state = 'Running' AND id IS NOT NULL ORDER BY waker_id;
+SELECT id, waker_id FROM y WHERE state = 'Running' ORDER BY waker_id;
 
 --
 -- Builds the parent-child chain from all thread_executing_spans. The parent is the waker and
@@ -206,10 +209,10 @@
 -- Note that this doesn't include the roots. We'll compute the roots below.
 -- This two step process improves performance because it's more efficient to scan
 -- parent and find a child between than to scan child and find the parent it lies between.
-CREATE PERFETTO TABLE _wakeup_chain
+CREATE PERFETTO TABLE _wakeup_graph
 AS
 SELECT
-  _waker_map.id AS parent_id,
+  _wakeup_map.id AS waker_id,
   prev_id,
   prev_end_ts,
   _wakeup.id AS id,
@@ -220,178 +223,259 @@
   _wakeup.state,
   _wakeup.blocked_function
 FROM _wakeup
-JOIN _waker_map USING(waker_id);
+JOIN _wakeup_map USING(waker_id)
+ORDER BY id;
 
 -- The inverse of thread_executing_spans. All the sleeping periods between thread_executing_spans.
-CREATE PERFETTO TABLE _sleeping_span
+CREATE PERFETTO TABLE _sleep
 AS
 WITH
   x AS (
     SELECT
       id,
       ts,
-      lag(end_ts) OVER (PARTITION BY utid ORDER BY id) AS prev_end_ts,
-      utid
-    FROM _wakeup_chain
+      prev_end_ts,
+      utid,
+      state,
+      blocked_function
+    FROM _wakeup_graph
   )
 SELECT
-  ts - prev_end_ts AS dur, prev_end_ts AS ts, id AS root_id, utid AS root_utid
+  ts - prev_end_ts AS dur,
+  prev_end_ts AS ts,
+  id AS root_node_id,
+  utid AS critical_path_utid,
+  id AS critical_path_id,
+  ts - prev_end_ts AS critical_path_blocked_dur,
+  state AS critical_path_blocked_state,
+  blocked_function AS critical_path_blocked_function
 FROM x
 WHERE ts IS NOT NULL;
 
---
--- Finds the roots of the |_wakeup_chain|.
-CREATE PERFETTO TABLE _wakeup_root
-AS
-WITH
-  _wakeup_root_id AS (
-    SELECT DISTINCT parent_id AS id FROM _wakeup_chain
-    EXCEPT
-    SELECT DISTINCT id FROM _wakeup_chain
+-- Given a set of critical paths identified by their |root_node_ids|, flattens
+-- the critical path tasks such that there are no overlapping intervals. The end of a
+-- task in the critical path is the start of the following task in the critical path.
+CREATE PERFETTO MACRO _flatten_critical_path_tasks(_critical_path_table TableOrSubquery)
+RETURNS TableOrSubquery
+AS (
+  WITH
+    x AS (
+      SELECT
+        LEAD(ts) OVER (PARTITION BY root_node_id ORDER BY node_id) AS ts,
+        node_id,
+        ts AS node_ts,
+        root_node_id,
+        utid AS node_utid,
+        _wakeup_graph.prev_end_ts
+      FROM $_critical_path_table
+      JOIN _wakeup_graph
+        ON node_id = id
+    )
+  SELECT node_ts AS ts, root_node_id, node_id, ts - node_ts AS dur, node_utid, prev_end_ts FROM x
+);
+
+-- Converts a table with <ts, dur, utid> columns to a unique set of wakeup roots <id> that
+-- completely cover the time intervals.
+CREATE PERFETTO MACRO _intervals_to_roots(source_table TableOrSubQuery)
+RETURNS TableOrSubQuery
+AS (
+  WITH source AS (
+    SELECT * FROM $source_table
+  ), thread_bounds AS (
+    SELECT utid, MIN(ts) AS min_start, MAX(ts) AS max_start FROM _wakeup_graph GROUP BY utid
+  ), start AS (
+    SELECT
+      _wakeup_graph.utid, max(_wakeup_graph.id) AS start_id, source.ts, source.dur
+      FROM _wakeup_graph
+      JOIN thread_bounds
+        USING (utid)
+      JOIN source
+        ON source.utid = _wakeup_graph.utid AND MAX(source.ts, min_start) >= _wakeup_graph.ts
+     GROUP BY source.ts, source.utid
+  ), end AS (
+    SELECT
+      _wakeup_graph.utid, min(_wakeup_graph.id) AS end_id, source.ts, source.dur
+      FROM _wakeup_graph
+      JOIN thread_bounds
+          USING (utid)
+      JOIN source ON source.utid = _wakeup_graph.utid
+          AND MIN((source.ts + source.dur), max_start) <= _wakeup_graph.ts
+     GROUP BY source.ts, source.utid
+  ), bound AS (
+    SELECT start.utid, start.ts, start.dur, start_id, end_id
+      FROM start
+      JOIN end ON start.ts = end.ts AND start.dur = end.dur AND start.utid = end.utid
   )
-SELECT NULL AS parent_id, _wakeup.*
-FROM _wakeup
-JOIN _wakeup_root_id USING(id);
+  SELECT DISTINCT _wakeup_graph.id FROM bound
+  JOIN _wakeup_graph ON _wakeup_graph.id BETWEEN start_id AND end_id
+);
 
---
--- Finds the leafs of the |_wakeup_chain|.
-CREATE PERFETTO TABLE _wakeup_leaf AS
-WITH
-  _wakeup_leaf_id AS (
-    SELECT DISTINCT id AS id FROM _wakeup_chain
-    EXCEPT
-    SELECT DISTINCT parent_id AS id FROM _wakeup_chain
-  )
-SELECT _wakeup_chain.*
-FROM _wakeup_chain
-JOIN _wakeup_leaf_id USING(id);
-
---
--- Merges the roots, leafs and the rest of the chain.
-CREATE TABLE _wakeup_graph
-AS
-SELECT
-  _wakeup_chain.parent_id,
-  _wakeup_chain.id,
-  _wakeup_chain.ts,
-  _wakeup_chain.end_ts - _wakeup_chain.ts AS dur,
-  _wakeup_chain.utid,
-  _wakeup_chain.prev_end_ts,
-  _wakeup_chain.state,
-  _wakeup_chain.blocked_function,
-  0 AS is_root,
-  (_wakeup_leaf.id IS NOT NULL) AS is_leaf
-FROM _wakeup_chain
-LEFT JOIN _wakeup_leaf
-  USING (id)
-UNION ALL
-SELECT
-  _wakeup_root.parent_id,
-  _wakeup_root.id,
-  _wakeup_root.ts,
-  _wakeup_root.end_ts - _wakeup_root.ts AS dur,
-  _wakeup_root.utid,
-  _wakeup_root.prev_end_ts,
-  _wakeup_root.state,
-  _wakeup_root.blocked_function,
-  1 AS is_root,
-  0 AS is_leaf
-FROM _wakeup_root;
-
--- Thread_executing_span graph of all wakeups across all processes.
---
--- @column root_id            Id of thread_executing_span that initiated the wakeup of |id|.
--- @column parent_id          Id of thread_executing_span that directly woke |id|.
--- @column id                 Id of the first (runnable) thread state in thread_executing_span.
--- @column ts                 Timestamp of first thread_state in thread_executing_span.
--- @column dur                Duration of thread_executing_span.
--- @column utid               Utid of thread with thread_state.
--- @column blocked_dur        Duration of blocking thread state before waking up.
--- @column blocked_state      Thread state ('D' or 'S') of blocked thread_state before waking up.
--- @column blocked_function   Kernel blocked_function of thread state before waking up.
--- @column is_root            Whether the thread_executing_span is a root.
--- @column depth              Tree depth of thread executing span from the root.
-CREATE TABLE _thread_executing_span_graph AS
-WITH roots AS (
-SELECT
-  id AS root_id,
-  parent_id,
-  id,
-  ts,
-  end_ts - ts AS dur,
-  utid,
-  ts - prev_end_ts AS blocked_dur,
-  state AS blocked_state,
-  blocked_function AS blocked_function,
-  1 AS is_root,
-  0 AS depth
-FROM _wakeup_root
-), chain AS (
-  SELECT * FROM roots
-  UNION ALL
+-- Flattens overlapping tasks within a critical path and flattens overlapping critical paths.
+CREATE PERFETTO MACRO _flatten_critical_paths(critical_path_table TableOrSubquery, sleeping_table TableOrSubquery)
+RETURNS TableOrSubquery
+AS (
+  WITH
+    span_starts AS (
+      SELECT
+        cr.node_utid AS utid,
+        MAX(cr.ts, sleep.ts) AS ts,
+        sleep.ts + sleep.dur AS sleep_end_ts,
+        cr.ts + cr.dur AS cr_end_ts,
+        cr.node_id AS id,
+        cr.root_node_id AS root_id,
+        cr.prev_end_ts AS prev_end_ts,
+        critical_path_utid,
+        critical_path_id,
+        critical_path_blocked_dur,
+        critical_path_blocked_state,
+        critical_path_blocked_function
+      FROM
+        _flatten_critical_path_tasks!($critical_path_table) cr
+      JOIN $sleeping_table sleep
+        USING (root_node_id)
+    )
   SELECT
-    chain.root_id,
-    graph.parent_id,
-    graph.id,
-    graph.ts,
-    graph.dur,
-    graph.utid,
-    graph.ts - graph.prev_end_ts AS blocked_dur,
-    graph.state AS blocked_state,
-    graph.blocked_function AS blocked_function,
-    0 AS is_root,
-    chain.depth + 1 AS depth
-  FROM _wakeup_graph graph
-  JOIN chain ON chain.id = graph.parent_id
-) SELECT chain.*, thread.upid FROM chain LEFT JOIN thread USING(utid);
-
--- It finds the MAX between the start of the critical span and the start
--- of the blocked region. This ensures that the critical path doesn't overlap
--- the preceding thread_executing_span before the blocked region.
-CREATE PERFETTO FUNCTION _critical_path_start_ts(ts LONG, leaf_blocked_ts LONG)
-RETURNS LONG AS SELECT MAX($ts, IFNULL($leaf_blocked_ts, $ts));
-
--- See |_thread_executing_span_critical_path|
-CREATE PERFETTO TABLE _critical_path
-AS
-WITH chain AS (
-  SELECT
-    parent_id,
+    ts,
+    MIN(cr_end_ts, sleep_end_ts) - ts AS dur,
+    utid,
     id,
+    root_id,
+    prev_end_ts,
+    critical_path_utid,
+    critical_path_id,
+    critical_path_blocked_dur,
+    critical_path_blocked_state,
+    critical_path_blocked_function
+  FROM span_starts
+  WHERE MIN(sleep_end_ts, cr_end_ts) - ts > 0
+);
+
+-- Generates a critical path.
+CREATE PERFETTO MACRO _critical_path(
+        graph_table TableOrSubquery, root_table TableOrSubquery, sleeping_table TableOrSubquery)
+RETURNS TableOrSubquery
+AS (
+  WITH
+    critical_path AS (
+      SELECT * FROM graph_reachable_weight_bounded_dfs !($graph_table, $root_table, 1)
+    )
+  SELECT
     ts,
     dur,
+    root_id,
+    id,
     utid,
-    id AS critical_path_id,
-    ts - blocked_dur AS critical_path_blocked_ts,
-    blocked_dur AS critical_path_blocked_dur,
-    blocked_state AS critical_path_blocked_state,
-    blocked_function AS critical_path_blocked_function,
-    utid AS critical_path_utid,
-    upid AS critical_path_upid
-  FROM _thread_executing_span_graph graph
+    critical_path_utid,
+    critical_path_id,
+    critical_path_blocked_dur,
+    critical_path_blocked_state,
+    critical_path_blocked_function
+  FROM _flatten_critical_paths!(critical_path, $sleeping_table)
   UNION ALL
+  -- Add roots
   SELECT
-    graph.parent_id,
-    graph.id,
-    _critical_path_start_ts(graph.ts, chain.critical_path_blocked_ts) AS ts,
-    MIN(_critical_path_start_ts(graph.ts, chain.critical_path_blocked_ts) + graph.dur, chain.ts)
-      - _critical_path_start_ts(graph.ts, chain.critical_path_blocked_ts) AS dur,
-    graph.utid,
-    chain.critical_path_id,
-    chain.critical_path_blocked_ts,
-    chain.critical_path_blocked_dur,
-    chain.critical_path_blocked_state,
-    chain.critical_path_blocked_function,
-    chain.critical_path_utid,
-    chain.critical_path_upid
-  FROM _thread_executing_span_graph graph
-  JOIN chain ON (chain.parent_id = graph.id AND (chain.ts > chain.critical_path_blocked_ts))
-) SELECT * FROM chain;
+    ts,
+    end_ts - ts AS dur,
+    id AS root_id,
+    id,
+    utid,
+    utid AS critical_path_utid,
+    NULL AS critical_path_id,
+    NULL AS critical_path_blocked_dur,
+    NULL AS critical_path_blocked_state,
+    NULL AS critical_path_blocked_function
+  FROM $root_table
+  ORDER BY root_id
+);
 
--- Thread executing span critical paths for all threads. For each thread, the critical path of
--- every sleeping thread state is computed and unioned with the thread executing spans on that thread.
--- The duration of a thread executing span in the critical path is the range between the start of the
--- thread_executing_span and the start of the next span in the critical path.
+-- Generates the critical path for only the set of roots <id> passed in.
+-- _intervals_to_roots can be used to generate root ids from a given time interval.
+-- This can be used to genrate the critical path over sparse regions of a trace, e.g
+-- binder transactions. It might be more efficient to generate the _critical_path
+-- for the entire trace, see _thread_executing_span_critical_path_all, but for a
+-- per-process susbset of binder txns for instance, this is likely faster.
+CREATE PERFETTO MACRO _critical_path_by_roots(roots_table TableOrSubQuery)
+RETURNS TableOrSubQuery
+AS (
+  WITH roots AS (
+    SELECT * FROM $roots_table
+  ), root_bounds AS (
+    SELECT MIN(id) AS min_root_id, MAX(id) AS max_root_id FROM roots
+  ), wakeup_bounds AS (
+    SELECT COALESCE(_wakeup_graph.prev_id, min_root_id) AS min_wakeup, max_root_id AS max_wakeup
+    FROM root_bounds
+    JOIN _wakeup_graph ON id = min_root_id
+  ) SELECT
+      id,
+      ts,
+      dur,
+      utid,
+      critical_path_id,
+      critical_path_blocked_dur,
+      critical_path_blocked_state,
+      critical_path_blocked_function,
+      critical_path_utid
+      FROM
+        _critical_path
+        !(
+          (
+            SELECT
+              id AS source_node_id,
+              COALESCE(waker_id, id) AS dest_node_id,
+              id - COALESCE(waker_id, id) AS edge_weight
+            FROM _wakeup_graph
+            JOIN wakeup_bounds WHERE id BETWEEN min_wakeup AND max_wakeup
+          ),
+          (
+            SELECT
+              _wakeup_graph.id AS root_node_id,
+              _wakeup_graph.id - COALESCE(prev_id, _wakeup_graph.id) AS root_target_weight,
+              id,
+              ts,
+              end_ts,
+              utid
+            FROM _wakeup_graph
+            JOIN (SELECT * FROM roots) USING (id)
+          ),
+          _sleep));
+
+-- Generates the critical path for only the time intervals for the utids given.
+-- Currently expensive because of naive interval_intersect implementation.
+-- Prefer _critical_paths_by_roots for performance. This is useful for a small
+-- set of intervals, e.g app startups in a trace.
+CREATE PERFETTO MACRO _critical_path_by_intervals(intervals_table TableOrSubQuery)
+RETURNS TableOrSubQuery AS (
+WITH span_starts AS (
+    SELECT
+      id,
+      MAX(span.ts, intervals.ts) AS ts,
+      MIN(span.ts + span.dur, intervals.ts + intervals.dur) AS end_ts,
+      span.utid,
+      critical_path_id,
+      critical_path_blocked_dur,
+      critical_path_blocked_state,
+      critical_path_blocked_function,
+      critical_path_utid
+    FROM _critical_path_by_roots!(_intervals_to_roots!($intervals_table)) span
+    -- TODO(zezeozue): Replace with interval_intersect when partitions are supported
+    JOIN (SELECT * FROM $intervals_table) intervals ON span.critical_path_utid = intervals.utid
+        AND ((span.ts BETWEEN intervals.ts AND intervals.ts + intervals.dur)
+             OR (intervals.ts BETWEEN span.ts AND span.ts + span.dur))
+) SELECT
+      id,
+      ts,
+      end_ts - ts AS dur,
+      utid,
+      critical_path_id,
+      critical_path_blocked_dur,
+      critical_path_blocked_state,
+      critical_path_blocked_function,
+      critical_path_utid
+   FROM span_starts);
+
+-- Generates the critical path for a given utid over the <ts, dur> interval.
+-- The duration of a thread executing span in the critical path is the range between the
+-- start of the thread_executing_span and the start of the next span in the critical path.
 CREATE PERFETTO FUNCTION _thread_executing_span_critical_path(
   -- Utid of the thread to compute the critical path for.
   critical_path_utid INT,
@@ -419,598 +503,41 @@
   -- Thread Utid the critical path was filtered to.
   critical_path_utid INT
 ) AS
-WITH span_starts AS (
-    SELECT
-      id,
-      MAX(ts, $ts) AS ts,
-      MIN(ts + dur, $ts + $dur) AS end_ts,
-      utid,
-      critical_path_id,
-      critical_path_blocked_dur,
-      critical_path_blocked_state,
-      critical_path_blocked_function,
-      critical_path_utid
-    FROM _critical_path span
-    WHERE (($critical_path_utid IS NOT NULL AND span.critical_path_utid = $critical_path_utid) OR ($critical_path_utid IS NULL))
-      AND ((ts BETWEEN $ts AND $ts + $dur) OR ($ts BETWEEN ts AND ts + dur))
-) SELECT
-      id,
-      ts,
-      end_ts - ts AS dur,
-      utid,
-      critical_path_id,
-      critical_path_blocked_dur,
-      critical_path_blocked_state,
-      critical_path_blocked_function,
-      critical_path_utid
-   FROM span_starts;
+SELECT * FROM _critical_path_by_intervals!((SELECT $critical_path_utid AS utid, $ts as ts, $dur AS dur));
 
--- Limited thread_state view that will later be span joined with the |_thread_executing_span_graph|.
-CREATE PERFETTO VIEW _span_thread_state_view
-AS SELECT id AS thread_state_id, ts, dur, utid, state, blocked_function as function, io_wait, cpu FROM thread_state;
-
--- |_thread_executing_span_graph| span joined with thread_state information.
-CREATE VIRTUAL TABLE _span_graph_thread_state_sp
-USING
-  SPAN_JOIN(
-    _thread_executing_span_graph PARTITIONED utid,
-    _span_thread_state_view PARTITIONED utid);
-
--- Limited slice_view that will later be span joined with the |_thread_executing_span_graph|.
-CREATE PERFETTO VIEW _span_slice_view
+-- Generates the critical path for all threads for the entire trace duration.
+-- The duration of a thread executing span in the critical path is the range between the
+-- start of the thread_executing_span and the start of the next span in the critical path.
+CREATE PERFETTO FUNCTION _thread_executing_span_critical_path_all()
+RETURNS
+  TABLE(
+    -- Id of the first (runnable) thread state in thread_executing_span.
+    id INT,
+    -- Timestamp of first thread_state in thread_executing_span.
+    ts LONG,
+    -- Duration of thread_executing_span.
+    dur LONG,
+    -- Utid of thread with thread_state.
+    utid INT,
+    -- Id of thread executing span following the sleeping thread state for which the critical path is computed.
+    critical_path_id INT,
+    -- Critical path duration.
+    critical_path_blocked_dur LONG,
+    -- Sleeping thread state in critical path.
+    critical_path_blocked_state STRING,
+    -- Kernel blocked_function of the critical path.
+    critical_path_blocked_function STRING,
+    -- Thread Utid the critical path was filtered to.
+    critical_path_utid INT)
 AS
 SELECT
-  slice_id,
-  depth AS slice_depth,
-  name AS slice_name,
-  CAST(ts AS INT) AS ts,
-  CAST(dur AS INT) AS dur,
-  utid
-FROM _slice_flattened;
-
--- |_thread_executing_span_graph| span joined with slice information.
-CREATE VIRTUAL TABLE _span_graph_slice_sp
-USING
-  SPAN_JOIN(
-    _thread_executing_span_graph PARTITIONED utid,
-    _span_slice_view PARTITIONED utid);
-
--- Limited |_thread_executing_span_graph| + thread_state view.
-CREATE PERFETTO VIEW _span_graph_thread_state
-AS
-SELECT ts, dur, id, thread_state_id, state, function, io_wait, cpu
-FROM _span_graph_thread_state_sp;
-
--- Limited |_thread_executing_span_graph| + slice view.
-CREATE PERFETTO VIEW _span_graph_slice
-AS
-SELECT ts, dur, id, slice_id, slice_depth, slice_name
-FROM _span_graph_slice_sp;
-
--- |_thread_executing_span_graph| + thread_state view joined with critical_path information.
-CREATE PERFETTO TABLE _critical_path_thread_state AS
-WITH span AS MATERIALIZED (
-    SELECT * FROM _critical_path
-  ),
-  span_starts AS (
-    SELECT
-      span.id,
-      span.utid,
-      span.critical_path_id,
-      span.critical_path_blocked_dur,
-      span.critical_path_blocked_state,
-      span.critical_path_blocked_function,
-      span.critical_path_utid,
-      thread_state_id,
-      MAX(thread_state.ts, span.ts) AS ts,
-      span.ts + span.dur AS span_end_ts,
-      thread_state.ts + thread_state.dur AS thread_state_end_ts,
-      thread_state.state,
-      thread_state.function,
-      thread_state.cpu,
-      thread_state.io_wait
-    FROM span
-    JOIN _span_graph_thread_state_sp thread_state USING(id)
-  )
-SELECT
   id,
-  thread_state_id,
   ts,
-  MIN(span_end_ts, thread_state_end_ts) - ts AS dur,
+  dur,
   utid,
-  state,
-  function,
-  cpu,
-  io_wait,
   critical_path_id,
   critical_path_blocked_dur,
   critical_path_blocked_state,
   critical_path_blocked_function,
   critical_path_utid
-FROM span_starts
-WHERE MIN(span_end_ts, thread_state_end_ts) - ts > 0;
-
--- |_thread_executing_span_graph| + thread_state + critical_path span joined with
--- |_thread_executing_span_graph| + slice view.
-CREATE VIRTUAL TABLE _critical_path_sp
-USING
-  SPAN_LEFT_JOIN(
-    _critical_path_thread_state PARTITIONED id,
-     _span_graph_slice PARTITIONED id);
-
--- Flattened slices span joined with their thread_states. This contains the 'self' information
--- without 'critical_path' (blocking) information.
-CREATE VIRTUAL TABLE _self_sp USING
-  SPAN_LEFT_JOIN(thread_state PARTITIONED utid, _slice_flattened PARTITIONED utid);
-
--- Limited view of |_self_sp|.
-CREATE PERFETTO VIEW _self_view
-  AS
-  SELECT
-    id AS self_thread_state_id,
-    slice_id AS self_slice_id,
-    ts,
-    dur,
-    utid AS critical_path_utid,
-    state AS self_state,
-    blocked_function AS self_function,
-    cpu AS self_cpu,
-    io_wait AS self_io_wait,
-    name AS self_slice_name,
-    depth AS self_slice_depth
-    FROM _self_sp;
-
--- Self and critical path span join. This contains the union of the time intervals from the following:
---  a. Self slice stack + thread_state.
---  b. Critical path stack + thread_state.
-CREATE VIRTUAL TABLE _self_and_critical_path_sp
-USING
-  SPAN_JOIN(
-    _self_view PARTITIONED critical_path_utid,
-    _critical_path_sp PARTITIONED critical_path_utid);
-
--- Returns a view of |_self_and_critical_path_sp| unpivoted over the following columns:
--- self thread_state.
--- self blocked_function (if one exists).
--- self process_name (enabled with |enable_process_name|).
--- self thread_name (enabled with |enable_thread_name|).
--- self slice_stack (enabled with |enable_self_slice|).
--- critical_path thread_state.
--- critical_path process_name.
--- critical_path thread_name.
--- critical_path slice_stack (enabled with |enable_critical_path_slice|).
--- running cpu (if one exists).
--- A 'stack' is the group of resulting unpivoted rows sharing the same timestamp.
-CREATE PERFETTO FUNCTION _critical_path_stack(critical_path_utid INT, ts LONG, dur LONG, enable_process_name INT, enable_thread_name INT, enable_self_slice INT, enable_critical_path_slice INT)
-RETURNS
-  TABLE(
-    id INT,
-    ts LONG,
-    dur LONG,
-    utid INT,
-    stack_depth INT,
-    name STRING,
-    table_name STRING,
-    critical_path_utid INT) AS
-  -- Spans filtered to the query time window and critical_path_utid.
-  -- This is a preliminary step that gets the start and end ts of all the rows
-  -- so that we can chop the ends of each interval correctly if it overlaps with the query time interval.
-  WITH relevant_spans_starts AS (
-    SELECT
-      self_thread_state_id,
-      self_state,
-      self_slice_id,
-      self_slice_name,
-      self_slice_depth,
-      self_function,
-      self_io_wait,
-      thread_state_id,
-      state,
-      function,
-      io_wait,
-      slice_id,
-      slice_name,
-      slice_depth,
-      cpu,
-      utid,
-      MAX(ts, $ts) AS ts,
-      MIN(ts + dur, $ts + $dur) AS end_ts,
-      critical_path_utid
-    FROM _self_and_critical_path_sp
-    WHERE dur > 0 AND critical_path_utid = $critical_path_utid
-  ),
-  -- This is the final step that gets the |dur| of each span from the start and
-  -- and end ts of the previous step.
-  -- Now we manually unpivot the result with 3 key steps: 1) Self 2) Critical path 3) CPU
-  -- This CTE is heavily used throughout the entire function so materializing it is
-  -- very important.
-  relevant_spans AS MATERIALIZED (
-    SELECT
-      self_thread_state_id,
-      self_state,
-      self_slice_id,
-      self_slice_name,
-      self_slice_depth,
-      self_function,
-      self_io_wait,
-      thread_state_id,
-      state,
-      function,
-      io_wait,
-      slice_id,
-      slice_name,
-      slice_depth,
-      cpu,
-      utid,
-      ts,
-      end_ts - ts AS dur,
-      critical_path_utid,
-      utid
-    FROM relevant_spans_starts
-    WHERE dur > 0
-  ),
-  -- 1. Builds the 'self' stack of items as an ordered UNION ALL
-  self_stack AS MATERIALIZED (
-    -- Builds the self thread_state
-    SELECT
-      self_thread_state_id AS id,
-      ts,
-      dur,
-      utid,
-      0 AS stack_depth,
-      'thread_state: ' || self_state AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM relevant_spans
-    UNION ALL
-    -- Builds the self kernel blocked_function
-    SELECT
-      self_thread_state_id AS id,
-      ts,
-      dur,
-      utid,
-      1 AS stack_depth,
-      IIF(self_state GLOB 'R*', NULL, 'kernel function: ' || self_function) AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM relevant_spans
-    UNION ALL
-    -- Builds the self kernel io_wait
-    SELECT
-      self_thread_state_id AS id,
-      ts,
-      dur,
-      utid,
-      2 AS stack_depth,
-      IIF(self_state GLOB 'R*', NULL, 'io_wait: ' || self_io_wait) AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM relevant_spans
-    UNION ALL
-    -- Builds the self process_name
-    SELECT
-      self_thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      3 AS stack_depth,
-      IIF($enable_process_name, 'process_name: ' || process.name, NULL) AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM relevant_spans
-    LEFT JOIN thread
-      ON thread.utid = critical_path_utid
-    LEFT JOIN process
-      USING (upid)
-    -- Builds the self thread_name
-    UNION ALL
-    SELECT
-      self_thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      4 AS stack_depth,
-      IIF($enable_thread_name, 'thread_name: ' || thread.name, NULL) AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM relevant_spans
-    LEFT JOIN thread
-      ON thread.utid = critical_path_utid
-    JOIN process
-      USING (upid)
-    UNION ALL
-    -- Builds the self 'ancestor' slice stack
-    SELECT
-      anc.id,
-      slice.ts,
-      slice.dur,
-      slice.utid,
-      anc.depth + 5 AS stack_depth,
-      IIF($enable_self_slice, anc.name, NULL) AS name,
-      'slice' AS table_name,
-      critical_path_utid
-    FROM relevant_spans slice
-    JOIN ancestor_slice(self_slice_id) anc WHERE anc.dur != -1
-    UNION ALL
-    -- Builds the self 'deepest' ancestor slice stack
-    SELECT
-      self_slice_id AS id,
-      ts,
-      dur,
-      utid,
-      self_slice_depth + 5 AS stack_depth,
-      IIF($enable_self_slice, self_slice_name, NULL) AS name,
-      'slice' AS table_name,
-      critical_path_utid
-    FROM relevant_spans slice
-    -- Ordering by stack depth is important to ensure the items can
-    -- be renedered in the UI as a debug track in the order in which
-    -- the sub-queries were 'unioned'.
-    ORDER BY stack_depth
-  ),
-  -- Prepares for stage 2 in building the entire stack.
-  -- Computes the starting depth for each stack. This is necessary because
-  -- each self slice stack has variable depth and the depth in each stack
-  -- most be contiguous in order to efficiently generate a pprof in the future.
-  critical_path_start_depth AS MATERIALIZED (
-    SELECT critical_path_utid, ts, MAX(stack_depth) + 1 AS start_depth
-    FROM self_stack
-    GROUP BY critical_path_utid, ts
-  ),
-  critical_path_span AS MATERIALIZED (
-    SELECT
-      thread_state_id,
-      state,
-      function,
-      io_wait,
-      slice_id,
-      slice_name,
-      slice_depth,
-      spans.ts,
-      spans.dur,
-      spans.critical_path_utid,
-      utid,
-      start_depth
-    FROM relevant_spans spans
-    JOIN critical_path_start_depth
-      ON
-        critical_path_start_depth.critical_path_utid = spans.critical_path_utid
-        AND critical_path_start_depth.ts = spans.ts
-    WHERE critical_path_start_depth.critical_path_utid = $critical_path_utid AND spans.critical_path_utid != spans.utid
-  ),
-  -- 2. Builds the 'critical_path' stack of items as an ordered UNION ALL
-  critical_path_stack AS MATERIALIZED (
-    -- Builds the critical_path thread_state
-    SELECT
-      thread_state_id AS id,
-      ts,
-      dur,
-      utid,
-      start_depth AS stack_depth,
-      'blocking thread_state: ' || state AS name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM critical_path_span
-    UNION ALL
-    -- Builds the critical_path process_name
-    SELECT
-      thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      start_depth + 1 AS stack_depth,
-      'blocking process_name: ' || process.name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM critical_path_span
-    JOIN thread USING (utid)
-    LEFT JOIN process USING (upid)
-    UNION ALL
-    -- Builds the critical_path thread_name
-    SELECT
-      thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      start_depth + 2 AS stack_depth,
-      'blocking thread_name: ' || thread.name,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM critical_path_span
-    JOIN thread USING (utid)
-    UNION ALL
-    -- Builds the critical_path kernel blocked_function
-    SELECT
-      thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      start_depth + 3 AS stack_depth,
-      'blocking kernel_function: ' || function,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM critical_path_span
-    JOIN thread USING (utid)
-    UNION ALL
-    -- Builds the critical_path kernel io_wait
-    SELECT
-      thread_state_id AS id,
-      ts,
-      dur,
-      thread.utid,
-      start_depth + 4 AS stack_depth,
-      'blocking io_wait: ' || io_wait,
-      'thread_state' AS table_name,
-      critical_path_utid
-    FROM critical_path_span
-    JOIN thread USING (utid)
-    UNION ALL
-    -- Builds the critical_path 'ancestor' slice stack
-    SELECT
-      anc.id,
-      slice.ts,
-      slice.dur,
-      slice.utid,
-      anc.depth + start_depth + 5 AS stack_depth,
-      IIF($enable_critical_path_slice, anc.name, NULL) AS name,
-      'slice' AS table_name,
-      critical_path_utid
-    FROM critical_path_span slice
-    JOIN ancestor_slice(slice_id) anc WHERE anc.dur != -1
-    UNION ALL
-    -- Builds the critical_path 'deepest' slice
-    SELECT
-      slice_id AS id,
-      ts,
-      dur,
-      utid,
-      slice_depth + start_depth + 5 AS stack_depth,
-      IIF($enable_critical_path_slice, slice_name, NULL) AS name,
-      'slice' AS table_name,
-      critical_path_utid
-    FROM critical_path_span slice
-    -- Ordering is also important as in the 'self' step above.
-    ORDER BY stack_depth
-  ),
-  -- Prepares for stage 3 in building the entire stack.
-  -- Computes the starting depth for each stack using the deepest stack_depth between
-  -- the critical_path stack and self stack. The self stack depth is
-  -- already computed and materialized in |critical_path_start_depth|.
-  cpu_start_depth_raw AS (
-    SELECT critical_path_utid, ts, MAX(stack_depth) + 1 AS start_depth
-    FROM critical_path_stack
-    GROUP BY critical_path_utid, ts
-    UNION ALL
-    SELECT * FROM critical_path_start_depth
-  ),
-  cpu_start_depth AS (
-    SELECT critical_path_utid, ts, MAX(start_depth) AS start_depth
-    FROM cpu_start_depth_raw
-    GROUP BY critical_path_utid, ts
-  ),
-  -- 3. Builds the 'CPU' stack for 'Running' states in either the self or critical path stack.
-  cpu_stack AS (
-    SELECT
-      thread_state_id AS id,
-      spans.ts,
-      spans.dur,
-      utid,
-      start_depth AS stack_depth,
-      'cpu: ' || cpu AS name,
-      'thread_state' AS table_name,
-      spans.critical_path_utid
-    FROM relevant_spans spans
-    JOIN cpu_start_depth
-      ON
-        cpu_start_depth.critical_path_utid = spans.critical_path_utid
-        AND cpu_start_depth.ts = spans.ts
-    WHERE cpu_start_depth.critical_path_utid = $critical_path_utid AND state = 'Running' OR self_state = 'Running'
-  ),
-  merged AS (
-    SELECT * FROM self_stack
-    UNION ALL
-    SELECT * FROM critical_path_stack
-    UNION ALL
-    SELECT * FROM cpu_stack
-  )
-SELECT * FROM merged WHERE id IS NOT NULL;
-
--- Critical path stack of thread_executing_spans with the following entities in the critical path
--- stacked from top to bottom: self thread_state, self blocked_function, self process_name,
--- self thread_name, slice stack, critical_path thread_state, critical_path process_name,
--- critical_path thread_name, critical_path slice_stack, running_cpu.
-CREATE PERFETTO FUNCTION _thread_executing_span_critical_path_stack(
-  -- Thread utid to filter critical paths to.
-  critical_path_utid INT,
-  -- Timestamp of start of time range to filter critical paths to.
-  ts LONG,
-  -- Duration of time range to filter critical paths to.
-  dur LONG)
-RETURNS
-  TABLE(
-    -- Id of the thread_state or slice in the thread_executing_span.
-    id INT,
-    -- Timestamp of slice in the critical path.
-    ts LONG,
-    -- Duration of slice in the critical path.
-    dur LONG,
-    -- Utid of thread that emitted the slice.
-    utid INT,
-    -- Stack depth of the entitity in the debug track.
-    stack_depth INT,
-    -- Name of entity in the critical path (could be a thread_state, kernel blocked_function, process_name, thread_name, slice name or cpu).
-    name STRING,
-    -- Table name of entity in the critical path (could be either slice or thread_state).
-    table_name STRING,
-    -- Utid of the thread the critical path was filtered to.
-    critical_path_utid INT
-) AS
-SELECT * FROM _critical_path_stack($critical_path_utid, $ts, $dur, 1, 1, 1, 1);
-
--- Returns a pprof aggregation of the stacks in |_critical_path_stack|.
-CREATE PERFETTO FUNCTION _critical_path_graph(graph_title STRING, critical_path_utid INT, ts LONG, dur LONG, enable_process_name INT, enable_thread_name INT, enable_self_slice INT, enable_critical_path_slice INT)
-RETURNS TABLE(pprof BYTES)
-AS
-WITH
-  stack AS MATERIALIZED (
-    SELECT
-      ts,
-      dur - IFNULL(LEAD(dur) OVER (PARTITION BY critical_path_utid, ts ORDER BY stack_depth), 0) AS dur,
-      name,
-      utid,
-      critical_path_utid,
-      stack_depth
-    FROM
-      _critical_path_stack($critical_path_utid, $ts, $dur, $enable_process_name, $enable_thread_name, $enable_self_slice, $enable_critical_path_slice)
-  ),
-  graph AS (
-    SELECT CAT_STACKS($graph_title) AS stack
-  ),
-  parent AS (
-    SELECT
-      cr.ts,
-      cr.dur,
-      cr.name,
-      cr.utid,
-      cr.stack_depth,
-      CAT_STACKS(graph.stack, cr.name) AS stack,
-      cr.critical_path_utid
-    FROM stack cr, graph
-    WHERE stack_depth = 0
-    UNION ALL
-    SELECT
-      child.ts,
-      child.dur,
-      child.name,
-      child.utid,
-      child.stack_depth,
-      CAT_STACKS(stack, child.name) AS stack,
-      child.critical_path_utid
-    FROM stack child
-    JOIN parent
-      ON
-        parent.critical_path_utid = child.critical_path_utid
-        AND parent.ts = child.ts
-        AND child.stack_depth = parent.stack_depth + 1
-  ),
-  stacks AS (
-    SELECT dur, stack FROM parent
-  )
-SELECT EXPERIMENTAL_PROFILE(stack, 'duration', 'ns', dur) AS pprof FROM stacks;
-
--- Returns a pprof aggreagation of the stacks in |_thread_executing_span_critical_path_stack|
-CREATE PERFETTO FUNCTION _thread_executing_span_critical_path_graph(
-  -- Descriptive name for the graph.
-  graph_title STRING,
-  -- Thread utid to filter critical paths to.
-  critical_path_utid INT,
-  -- Timestamp of start of time range to filter critical paths to.
-  ts INT,
-  -- Duration of time range to filter critical paths to.
-  dur INT)
-RETURNS TABLE(
-  -- Pprof of critical path stacks.
-  pprof BYTES
-)
-AS
-SELECT * FROM _critical_path_graph($graph_title, $critical_path_utid, $ts, $dur, 1, 1, 1, 1);
+FROM _critical_path_by_roots!((SELECT id FROM _wakeup_graph));
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span_with_slice.sql b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span_with_slice.sql
new file mode 100644
index 0000000..89085f3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span_with_slice.sql
@@ -0,0 +1,573 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+INCLUDE PERFETTO MODULE slices.flat_slices;
+INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+CREATE PERFETTO TABLE _critical_path_all AS
+SELECT * FROM  _thread_executing_span_critical_path_all();
+
+-- Limited thread_state view that will later be span joined with the |_thread_executing_span_graph|.
+CREATE PERFETTO VIEW _span_thread_state_view
+AS SELECT id AS thread_state_id, ts, dur, utid, state, blocked_function as function, io_wait, cpu FROM thread_state;
+
+-- Limited slice_view that will later be span joined with the |_thread_executing_span_graph|.
+CREATE PERFETTO VIEW _span_slice_view
+AS
+SELECT
+  slice_id,
+  depth AS slice_depth,
+  name AS slice_name,
+  CAST(ts AS INT) AS ts,
+  CAST(dur AS INT) AS dur,
+  utid
+FROM _slice_flattened;
+
+CREATE VIRTUAL TABLE _span_thread_state_slice_view
+USING
+  SPAN_LEFT_JOIN(
+    _span_thread_state_view PARTITIONED utid,
+    _span_slice_view PARTITIONED utid);
+
+-- |_thread_executing_span_graph| span joined with thread_state information.
+CREATE VIRTUAL TABLE _span_critical_path_thread_state_slice_sp
+USING
+  SPAN_JOIN(
+    _critical_path_all PARTITIONED utid,
+    _span_thread_state_slice_view PARTITIONED utid);
+
+-- |_thread_executing_span_graph| + thread_state view joined with critical_path information.
+CREATE PERFETTO TABLE _critical_path_thread_state_slice AS
+WITH span_starts AS (
+    SELECT
+      span.id,
+      span.utid,
+      span.critical_path_id,
+      span.critical_path_blocked_dur,
+      span.critical_path_blocked_state,
+      span.critical_path_blocked_function,
+      span.critical_path_utid,
+      thread_state_id,
+      MAX(thread_state.ts, span.ts) AS ts,
+      span.ts + span.dur AS span_end_ts,
+      thread_state.ts + thread_state.dur AS thread_state_end_ts,
+      thread_state.state,
+      thread_state.function,
+      thread_state.cpu,
+      thread_state.io_wait,
+      thread_state.slice_id,
+      thread_state.slice_name,
+      thread_state.slice_depth
+    FROM _critical_path_all span
+    JOIN _span_critical_path_thread_state_slice_sp thread_state USING(id)
+  )
+SELECT
+  id,
+  thread_state_id,
+  ts,
+  MIN(span_end_ts, thread_state_end_ts) - ts AS dur,
+  utid,
+  state,
+  function,
+  cpu,
+  io_wait,
+  slice_id,
+  slice_name,
+  slice_depth,
+  critical_path_id,
+  critical_path_blocked_dur,
+  critical_path_blocked_state,
+  critical_path_blocked_function,
+  critical_path_utid
+FROM span_starts
+WHERE MIN(span_end_ts, thread_state_end_ts) - ts > 0;
+
+-- Flattened slices span joined with their thread_states. This contains the 'self' information
+-- without 'critical_path' (blocking) information.
+CREATE VIRTUAL TABLE _self_sp USING
+  SPAN_LEFT_JOIN(thread_state PARTITIONED utid, _slice_flattened PARTITIONED utid);
+
+-- Limited view of |_self_sp|.
+CREATE PERFETTO VIEW _self_view
+  AS
+  SELECT
+    id AS self_thread_state_id,
+    slice_id AS self_slice_id,
+    ts,
+    dur,
+    utid AS critical_path_utid,
+    state AS self_state,
+    blocked_function AS self_function,
+    cpu AS self_cpu,
+    io_wait AS self_io_wait,
+    name AS self_slice_name,
+    depth AS self_slice_depth
+    FROM _self_sp;
+
+-- Self and critical path span join. This contains the union of the time intervals from the following:
+--  a. Self slice stack + thread_state.
+--  b. Critical path stack + thread_state.
+CREATE VIRTUAL TABLE _self_and_critical_path_sp
+USING
+  SPAN_JOIN(
+    _self_view PARTITIONED critical_path_utid,
+    _critical_path_thread_state_slice PARTITIONED critical_path_utid);
+
+-- Returns a view of |_self_and_critical_path_sp| unpivoted over the following columns:
+-- self thread_state.
+-- self blocked_function (if one exists).
+-- self process_name (enabled with |enable_process_name|).
+-- self thread_name (enabled with |enable_thread_name|).
+-- self slice_stack (enabled with |enable_self_slice|).
+-- critical_path thread_state.
+-- critical_path process_name.
+-- critical_path thread_name.
+-- critical_path slice_stack (enabled with |enable_critical_path_slice|).
+-- running cpu (if one exists).
+-- A 'stack' is the group of resulting unpivoted rows sharing the same timestamp.
+CREATE PERFETTO FUNCTION _critical_path_stack(critical_path_utid INT, ts LONG, dur LONG, enable_process_name INT, enable_thread_name INT, enable_self_slice INT, enable_critical_path_slice INT)
+RETURNS
+  TABLE(
+    id INT,
+    ts LONG,
+    dur LONG,
+    utid INT,
+    stack_depth INT,
+    name STRING,
+    table_name STRING,
+    critical_path_utid INT) AS
+  -- Spans filtered to the query time window and critical_path_utid.
+  -- This is a preliminary step that gets the start and end ts of all the rows
+  -- so that we can chop the ends of each interval correctly if it overlaps with the query time interval.
+  WITH relevant_spans_starts AS (
+    SELECT
+      self_thread_state_id,
+      self_state,
+      self_slice_id,
+      self_slice_name,
+      self_slice_depth,
+      self_function,
+      self_io_wait,
+      thread_state_id,
+      state,
+      function,
+      io_wait,
+      slice_id,
+      slice_name,
+      slice_depth,
+      cpu,
+      utid,
+      MAX(ts, $ts) AS ts,
+      MIN(ts + dur, $ts + $dur) AS end_ts,
+      critical_path_utid
+    FROM _self_and_critical_path_sp
+    WHERE dur > 0 AND critical_path_utid = $critical_path_utid
+  ),
+  -- This is the final step that gets the |dur| of each span from the start and
+  -- and end ts of the previous step.
+  -- Now we manually unpivot the result with 3 key steps: 1) Self 2) Critical path 3) CPU
+  -- This CTE is heavily used throughout the entire function so materializing it is
+  -- very important.
+  relevant_spans AS MATERIALIZED (
+    SELECT
+      self_thread_state_id,
+      self_state,
+      self_slice_id,
+      self_slice_name,
+      self_slice_depth,
+      self_function,
+      self_io_wait,
+      thread_state_id,
+      state,
+      function,
+      io_wait,
+      slice_id,
+      slice_name,
+      slice_depth,
+      cpu,
+      utid,
+      ts,
+      end_ts - ts AS dur,
+      critical_path_utid,
+      utid
+    FROM relevant_spans_starts
+    WHERE dur > 0
+  ),
+  -- 1. Builds the 'self' stack of items as an ordered UNION ALL
+  self_stack AS MATERIALIZED (
+    -- Builds the self thread_state
+    SELECT
+      self_thread_state_id AS id,
+      ts,
+      dur,
+      utid,
+      0 AS stack_depth,
+      'thread_state: ' || self_state AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM relevant_spans
+    UNION ALL
+    -- Builds the self kernel blocked_function
+    SELECT
+      self_thread_state_id AS id,
+      ts,
+      dur,
+      utid,
+      1 AS stack_depth,
+      IIF(self_state GLOB 'R*', NULL, 'kernel function: ' || self_function) AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM relevant_spans
+    UNION ALL
+    -- Builds the self kernel io_wait
+    SELECT
+      self_thread_state_id AS id,
+      ts,
+      dur,
+      utid,
+      2 AS stack_depth,
+      IIF(self_state GLOB 'R*', NULL, 'io_wait: ' || self_io_wait) AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM relevant_spans
+    UNION ALL
+    -- Builds the self process_name
+    SELECT
+      self_thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      3 AS stack_depth,
+      IIF($enable_process_name, 'process_name: ' || process.name, NULL) AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM relevant_spans
+    LEFT JOIN thread
+      ON thread.utid = critical_path_utid
+    LEFT JOIN process
+      USING (upid)
+    -- Builds the self thread_name
+    UNION ALL
+    SELECT
+      self_thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      4 AS stack_depth,
+      IIF($enable_thread_name, 'thread_name: ' || thread.name, NULL) AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM relevant_spans
+    LEFT JOIN thread
+      ON thread.utid = critical_path_utid
+    JOIN process
+      USING (upid)
+    UNION ALL
+    -- Builds the self 'ancestor' slice stack
+    SELECT
+      anc.id,
+      slice.ts,
+      slice.dur,
+      slice.utid,
+      anc.depth + 5 AS stack_depth,
+      IIF($enable_self_slice, anc.name, NULL) AS name,
+      'slice' AS table_name,
+      critical_path_utid
+    FROM relevant_spans slice
+    JOIN ancestor_slice(self_slice_id) anc WHERE anc.dur != -1
+    UNION ALL
+    -- Builds the self 'deepest' ancestor slice stack
+    SELECT
+      self_slice_id AS id,
+      ts,
+      dur,
+      utid,
+      self_slice_depth + 5 AS stack_depth,
+      IIF($enable_self_slice, self_slice_name, NULL) AS name,
+      'slice' AS table_name,
+      critical_path_utid
+    FROM relevant_spans slice
+    -- Ordering by stack depth is important to ensure the items can
+    -- be renedered in the UI as a debug track in the order in which
+    -- the sub-queries were 'unioned'.
+    ORDER BY stack_depth
+  ),
+  -- Prepares for stage 2 in building the entire stack.
+  -- Computes the starting depth for each stack. This is necessary because
+  -- each self slice stack has variable depth and the depth in each stack
+  -- most be contiguous in order to efficiently generate a pprof in the future.
+  critical_path_start_depth AS MATERIALIZED (
+    SELECT critical_path_utid, ts, MAX(stack_depth) + 1 AS start_depth
+    FROM self_stack
+    GROUP BY critical_path_utid, ts
+  ),
+  critical_path_span AS MATERIALIZED (
+    SELECT
+      thread_state_id,
+      state,
+      function,
+      io_wait,
+      slice_id,
+      slice_name,
+      slice_depth,
+      spans.ts,
+      spans.dur,
+      spans.critical_path_utid,
+      utid,
+      start_depth
+    FROM relevant_spans spans
+    JOIN critical_path_start_depth
+      ON
+        critical_path_start_depth.critical_path_utid = spans.critical_path_utid
+        AND critical_path_start_depth.ts = spans.ts
+    WHERE critical_path_start_depth.critical_path_utid = $critical_path_utid AND spans.critical_path_utid != spans.utid
+  ),
+  -- 2. Builds the 'critical_path' stack of items as an ordered UNION ALL
+  critical_path_stack AS MATERIALIZED (
+    -- Builds the critical_path thread_state
+    SELECT
+      thread_state_id AS id,
+      ts,
+      dur,
+      utid,
+      start_depth AS stack_depth,
+      'blocking thread_state: ' || state AS name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM critical_path_span
+    UNION ALL
+    -- Builds the critical_path process_name
+    SELECT
+      thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      start_depth + 1 AS stack_depth,
+      'blocking process_name: ' || process.name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM critical_path_span
+    JOIN thread USING (utid)
+    LEFT JOIN process USING (upid)
+    UNION ALL
+    -- Builds the critical_path thread_name
+    SELECT
+      thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      start_depth + 2 AS stack_depth,
+      'blocking thread_name: ' || thread.name,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM critical_path_span
+    JOIN thread USING (utid)
+    UNION ALL
+    -- Builds the critical_path kernel blocked_function
+    SELECT
+      thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      start_depth + 3 AS stack_depth,
+      'blocking kernel_function: ' || function,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM critical_path_span
+    JOIN thread USING (utid)
+    UNION ALL
+    -- Builds the critical_path kernel io_wait
+    SELECT
+      thread_state_id AS id,
+      ts,
+      dur,
+      thread.utid,
+      start_depth + 4 AS stack_depth,
+      'blocking io_wait: ' || io_wait,
+      'thread_state' AS table_name,
+      critical_path_utid
+    FROM critical_path_span
+    JOIN thread USING (utid)
+    UNION ALL
+    -- Builds the critical_path 'ancestor' slice stack
+    SELECT
+      anc.id,
+      slice.ts,
+      slice.dur,
+      slice.utid,
+      anc.depth + start_depth + 5 AS stack_depth,
+      IIF($enable_critical_path_slice, anc.name, NULL) AS name,
+      'slice' AS table_name,
+      critical_path_utid
+    FROM critical_path_span slice
+    JOIN ancestor_slice(slice_id) anc WHERE anc.dur != -1
+    UNION ALL
+    -- Builds the critical_path 'deepest' slice
+    SELECT
+      slice_id AS id,
+      ts,
+      dur,
+      utid,
+      slice_depth + start_depth + 5 AS stack_depth,
+      IIF($enable_critical_path_slice, slice_name, NULL) AS name,
+      'slice' AS table_name,
+      critical_path_utid
+    FROM critical_path_span slice
+    -- Ordering is also important as in the 'self' step above.
+    ORDER BY stack_depth
+  ),
+  -- Prepares for stage 3 in building the entire stack.
+  -- Computes the starting depth for each stack using the deepest stack_depth between
+  -- the critical_path stack and self stack. The self stack depth is
+  -- already computed and materialized in |critical_path_start_depth|.
+  cpu_start_depth_raw AS (
+    SELECT critical_path_utid, ts, MAX(stack_depth) + 1 AS start_depth
+    FROM critical_path_stack
+    GROUP BY critical_path_utid, ts
+    UNION ALL
+    SELECT * FROM critical_path_start_depth
+  ),
+  cpu_start_depth AS (
+    SELECT critical_path_utid, ts, MAX(start_depth) AS start_depth
+    FROM cpu_start_depth_raw
+    GROUP BY critical_path_utid, ts
+  ),
+  -- 3. Builds the 'CPU' stack for 'Running' states in either the self or critical path stack.
+  cpu_stack AS (
+    SELECT
+      thread_state_id AS id,
+      spans.ts,
+      spans.dur,
+      utid,
+      start_depth AS stack_depth,
+      'cpu: ' || cpu AS name,
+      'thread_state' AS table_name,
+      spans.critical_path_utid
+    FROM relevant_spans spans
+    JOIN cpu_start_depth
+      ON
+        cpu_start_depth.critical_path_utid = spans.critical_path_utid
+        AND cpu_start_depth.ts = spans.ts
+    WHERE cpu_start_depth.critical_path_utid = $critical_path_utid AND state = 'Running' OR self_state = 'Running'
+  ),
+  merged AS (
+    SELECT * FROM self_stack
+    UNION ALL
+    SELECT * FROM critical_path_stack
+    UNION ALL
+    SELECT * FROM cpu_stack
+  )
+SELECT * FROM merged WHERE id IS NOT NULL;
+
+-- Critical path stack of thread_executing_spans with the following entities in the critical path
+-- stacked from top to bottom: self thread_state, self blocked_function, self process_name,
+-- self thread_name, slice stack, critical_path thread_state, critical_path process_name,
+-- critical_path thread_name, critical_path slice_stack, running_cpu.
+CREATE PERFETTO FUNCTION _thread_executing_span_critical_path_stack(
+  -- Thread utid to filter critical paths to.
+  critical_path_utid INT,
+  -- Timestamp of start of time range to filter critical paths to.
+  ts LONG,
+  -- Duration of time range to filter critical paths to.
+  dur LONG)
+RETURNS
+  TABLE(
+    -- Id of the thread_state or slice in the thread_executing_span.
+    id INT,
+    -- Timestamp of slice in the critical path.
+    ts LONG,
+    -- Duration of slice in the critical path.
+    dur LONG,
+    -- Utid of thread that emitted the slice.
+    utid INT,
+    -- Stack depth of the entitity in the debug track.
+    stack_depth INT,
+    -- Name of entity in the critical path (could be a thread_state, kernel blocked_function, process_name, thread_name, slice name or cpu).
+    name STRING,
+    -- Table name of entity in the critical path (could be either slice or thread_state).
+    table_name STRING,
+    -- Utid of the thread the critical path was filtered to.
+    critical_path_utid INT
+) AS
+SELECT * FROM _critical_path_stack($critical_path_utid, $ts, $dur, 1, 1, 1, 1);
+
+-- Returns a pprof aggregation of the stacks in |_critical_path_stack|.
+CREATE PERFETTO FUNCTION _critical_path_graph(graph_title STRING, critical_path_utid INT, ts LONG, dur LONG, enable_process_name INT, enable_thread_name INT, enable_self_slice INT, enable_critical_path_slice INT)
+RETURNS TABLE(pprof BYTES)
+AS
+WITH
+  stack AS MATERIALIZED (
+    SELECT
+      ts,
+      dur - IFNULL(LEAD(dur) OVER (PARTITION BY critical_path_utid, ts ORDER BY stack_depth), 0) AS dur,
+      name,
+      utid,
+      critical_path_utid,
+      stack_depth
+    FROM
+      _critical_path_stack($critical_path_utid, $ts, $dur, $enable_process_name, $enable_thread_name, $enable_self_slice, $enable_critical_path_slice)
+  ),
+  graph AS (
+    SELECT CAT_STACKS($graph_title) AS stack
+  ),
+  parent AS (
+    SELECT
+      cr.ts,
+      cr.dur,
+      cr.name,
+      cr.utid,
+      cr.stack_depth,
+      CAT_STACKS(graph.stack, cr.name) AS stack,
+      cr.critical_path_utid
+    FROM stack cr, graph
+    WHERE stack_depth = 0
+    UNION ALL
+    SELECT
+      child.ts,
+      child.dur,
+      child.name,
+      child.utid,
+      child.stack_depth,
+      CAT_STACKS(stack, child.name) AS stack,
+      child.critical_path_utid
+    FROM stack child
+    JOIN parent
+      ON
+        parent.critical_path_utid = child.critical_path_utid
+        AND parent.ts = child.ts
+        AND child.stack_depth = parent.stack_depth + 1
+  ),
+  stacks AS (
+    SELECT dur, stack FROM parent
+  )
+SELECT EXPERIMENTAL_PROFILE(stack, 'duration', 'ns', dur) AS pprof FROM stacks;
+
+-- Returns a pprof aggreagation of the stacks in |_thread_executing_span_critical_path_stack|
+CREATE PERFETTO FUNCTION _thread_executing_span_critical_path_graph(
+  -- Descriptive name for the graph.
+  graph_title STRING,
+  -- Thread utid to filter critical paths to.
+  critical_path_utid INT,
+  -- Timestamp of start of time range to filter critical paths to.
+  ts INT,
+  -- Duration of time range to filter critical paths to.
+  dur INT)
+RETURNS TABLE(
+  -- Pprof of critical path stacks.
+  pprof BYTES
+)
+AS
+SELECT * FROM _critical_path_graph($graph_title, $critical_path_utid, $ts, $dur, 1, 1, 1, 1);
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index fe67ac8..9f67428 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -42,6 +42,10 @@
     "proto_util.h",
     "prune_package_list.cc",
     "prune_package_list.h",
+    "redact_sched_switch.cc",
+    "redact_sched_switch.h",
+    "redact_sched_waking.cc",
+    "redact_sched_waking.h",
     "scrub_ftrace_events.cc",
     "scrub_ftrace_events.h",
     "scrub_process_trees.cc",
@@ -61,9 +65,7 @@
     "../../include/perfetto/ext/base",
     "../../include/perfetto/protozero:protozero",
     "../../include/perfetto/trace_processor:storage",
-    "../../protos/perfetto/trace:non_minimal_cpp",
     "../../protos/perfetto/trace:non_minimal_zero",
-    "../../protos/perfetto/trace/android:cpp",
     "../../protos/perfetto/trace/android:zero",
     "../../protos/perfetto/trace/ftrace:zero",
     "../../protos/perfetto/trace/ps:zero",
@@ -74,8 +76,11 @@
 source_set("integrationtests") {
   testonly = true
   sources = [
+    "redact_sched_switch_integrationtest.cc",
+    "redact_sched_waking_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
+    "scrub_task_rename_integrationtest.cc",
     "trace_redactor_integrationtest.cc",
   ]
   deps = [
@@ -99,6 +104,8 @@
     "process_thread_timeline_unittest.cc",
     "proto_util_unittest.cc",
     "prune_package_list_unittest.cc",
+    "redact_sched_switch_unittest.cc",
+    "redact_sched_waking_unittest.cc",
     "scrub_ftrace_events_unittest.cc",
     "scrub_task_rename_unittest.cc",
     "scrub_trace_packet_unittest.cc",
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index e8cea13..767e90e 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -21,6 +21,8 @@
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/prune_package_list.h"
+#include "src/trace_redaction/redact_sched_switch.h"
+#include "src/trace_redaction/redact_sched_waking.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_task_rename.h"
@@ -50,6 +52,8 @@
   redactor.transformers()->emplace_back(new ScrubFtraceEvents());
   redactor.transformers()->emplace_back(new ScrubProcessTrees());
   redactor.transformers()->emplace_back(new ScrubTaskRename());
+  redactor.transformers()->emplace_back(new RedactSchedSwitch());
+  redactor.transformers()->emplace_back(new RedactSchedWaking());
 
   Context context;
   context.package_name = package_name;
diff --git a/src/trace_redaction/prune_package_list.cc b/src/trace_redaction/prune_package_list.cc
index a85a18f..de6331b 100644
--- a/src/trace_redaction/prune_package_list.cc
+++ b/src/trace_redaction/prune_package_list.cc
@@ -18,15 +18,29 @@
 
 #include <string>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 
-#include "protos/perfetto/trace/android/packages_list.gen.h"
-#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/android/packages_list.pbzero.h"
+#include "src/trace_redaction/proto_util.h"
 
 namespace perfetto::trace_redaction {
+namespace {
 
-PrunePackageList::PrunePackageList() = default;
-PrunePackageList::~PrunePackageList() = default;
+bool ShouldKeepPackageInfo(protozero::Field package_info, uint64_t uid) {
+  PERFETTO_DCHECK(package_info.id() ==
+                  protos::pbzero::PackagesList::kPackagesFieldNumber);
+
+  protozero::ProtoDecoder decoder(package_info.as_bytes());
+  auto uid_field = decoder.FindField(
+      protos::pbzero::PackagesList::PackageInfo::kUidFieldNumber);
+
+  return uid_field.valid() &&
+         NormalizeUid(uid_field.as_uint64()) == NormalizeUid(uid);
+}
+
+}  // namespace
 
 base::Status PrunePackageList::Transform(const Context& context,
                                          std::string* packet) const {
@@ -34,30 +48,47 @@
     return base::ErrStatus("PrunePackageList: missing package uid.");
   }
 
+  protozero::ProtoDecoder packet_decoder(*packet);
+
   protos::pbzero::TracePacket::Decoder trace_packet_decoder(*packet);
 
-  if (!trace_packet_decoder.has_packages_list()) {
+  auto package_list = packet_decoder.FindField(
+      protos::pbzero::TracePacket::kPackagesListFieldNumber);
+
+  if (!package_list.valid()) {
     return base::OkStatus();
   }
 
-  auto normalized_uid = NormalizeUid(context.package_uid.value());
+  auto uid = context.package_uid.value();
 
-  protos::gen::TracePacket mutable_packet;
-  mutable_packet.ParseFromString(*packet);
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
 
-  auto* packages = mutable_packet.mutable_packages_list()->mutable_packages();
+  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
+       packet_field = packet_decoder.ReadField()) {
+    if (packet_field.id() !=
+        protos::pbzero::TracePacket::kPackagesListFieldNumber) {
+      proto_util::AppendField(packet_field, packet_message.get());
+      continue;
+    }
 
-  // Remove all entries that don't match the uid. After this, one or more
-  // packages will be left in the list (multiple packages can share a uid).
-  packages->erase(
-      std::remove_if(
-          packages->begin(), packages->end(),
-          [normalized_uid](const protos::gen::PackagesList::PackageInfo& info) {
-            return NormalizeUid(info.uid()) != normalized_uid;
-          }),
-      packages->end());
+    auto* package_list_message = packet_message->set_packages_list();
 
-  packet->assign(mutable_packet.SerializeAsString());
+    protozero::ProtoDecoder package_list_decoder(packet_field.as_bytes());
+
+    for (auto package_field = package_list_decoder.ReadField();
+         package_field.valid();
+         package_field = package_list_decoder.ReadField()) {
+      // If not packages, keep.
+      // If packages and uid matches, keep.
+      if (package_field.id() !=
+              protos::pbzero::PackagesList::kPackagesFieldNumber ||
+          ShouldKeepPackageInfo(package_field, uid)) {
+        proto_util::AppendField(package_field, package_list_message);
+      }
+    }
+  }
+
+  packet->assign(packet_message.SerializeAsString());
 
   return base::OkStatus();
 }
diff --git a/src/trace_redaction/prune_package_list.h b/src/trace_redaction/prune_package_list.h
index 24d9ec2..ff5f060 100644
--- a/src/trace_redaction/prune_package_list.h
+++ b/src/trace_redaction/prune_package_list.h
@@ -28,9 +28,6 @@
 // Returns `base::ErrStatus()` if `Context.package_uid` was not set.
 class PrunePackageList final : public TransformPrimitive {
  public:
-  PrunePackageList();
-  ~PrunePackageList() override;
-
   base::Status Transform(const Context& context,
                          std::string* packet) const override;
 };
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
new file mode 100644
index 0000000..b70ef7a
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/redact_sched_switch.h"
+
+#include <string>
+
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/proto_util.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+// Redact sched switch trace events in an ftrace event bundle:
+//
+//  event {
+//    timestamp: 6702093744772646
+//    pid: 0
+//    sched_switch {
+//      prev_comm: "swapper/0"
+//      prev_pid: 0
+//      prev_prio: 120
+//      prev_state: 0
+//      next_comm: "writer"
+//      next_pid: 23020
+//      next_prio: 96
+//    }
+//  }
+//
+// In the above message, it should be noted that "event.pid" will always be
+// equal to "event.sched_switch.prev_pid".
+//
+// "ftrace_event_bundle_message" is the ftrace event bundle (contains a
+// collection of ftrace event messages) because data in a sched_switch message
+// is needed in order to know if the event should be added to the bundle.
+void RedactSwitchEvent(
+    const Context& context,
+    protos::pbzero::FtraceEvent::Decoder event,
+    protos::pbzero::FtraceEventBundle* ftrace_event_bundle_message) {
+  PERFETTO_DCHECK(context.timeline);
+  PERFETTO_DCHECK(context.package_uid.has_value());
+  PERFETTO_DCHECK(event.has_sched_switch());
+  PERFETTO_DCHECK(ftrace_event_bundle_message);
+
+  // If there is no timestamp in the event, it is not possible to query the
+  // timeline. This is too risky to keep.
+  if (!event.has_timestamp()) {
+    return;
+  }
+
+  protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+      event.sched_switch());
+
+  // There must be a prev pid and a next pid. Otherwise, the event is invalid.
+  // Dropping the event is the safest option.
+  if (!sched_switch.has_prev_pid() || !sched_switch.has_next_pid()) {
+    return;
+  }
+
+  auto uid = context.package_uid.value();
+
+  auto prev_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.prev_pid());
+  auto next_slice =
+      context.timeline->Search(event.timestamp(), sched_switch.next_pid());
+
+  // Build a new event, clearing the comm values when needed.
+  auto* event_message = ftrace_event_bundle_message->add_event();
+
+  // Reset to scan event fields.
+  event.Reset();
+
+  for (auto event_field = event.ReadField(); event_field.valid();
+       event_field = event.ReadField()) {
+    // This primitive only needs to affect sched switch events.
+    if (event_field.id() !=
+        protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+      proto_util::AppendField(event_field, event_message);
+      continue;
+    }
+
+    // Reset to scan sched_switch fields.
+    sched_switch.Reset();
+
+    auto switch_message = event_message->set_sched_switch();
+
+    for (auto switch_field = sched_switch.ReadField(); switch_field.valid();
+         switch_field = sched_switch.ReadField()) {
+      switch (switch_field.id()) {
+        case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
+          if (prev_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+
+        case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber: {
+          if (next_slice.uid == uid) {
+            proto_util::AppendField(switch_field, switch_message);
+          }
+          break;
+        }
+
+        default:
+          proto_util::AppendField(switch_field, switch_message);
+          break;
+      }
+    }
+  }
+}
+
+}  // namespace
+
+base::Status RedactSchedSwitch::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (packet == nullptr || packet->empty()) {
+    return base::ErrStatus("RedactSchedSwitch: null or empty packet.");
+  }
+
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("RedactSchedSwitch: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("RedactSchedSwitch: missing timeline.");
+  }
+
+  protozero::ProtoDecoder packet_decoder(*packet);
+
+  auto trace_event_bundle = packet_decoder.FindField(
+      protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+  if (!trace_event_bundle.valid()) {
+    return base::OkStatus();
+  }
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
+
+  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
+       packet_field = packet_decoder.ReadField()) {
+    if (packet_field.id() !=
+        protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      proto_util::AppendField(packet_field, packet_message.get());
+      continue;
+    }
+
+    protozero::ProtoDecoder bundle(packet_field.as_bytes());
+
+    auto* bundle_message = packet_message->set_ftrace_events();
+
+    for (auto field = bundle.ReadField(); field.valid();
+         field = bundle.ReadField()) {
+      if (field.id() != protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+        proto_util::AppendField(field, bundle_message);
+        continue;
+      }
+
+      protos::pbzero::FtraceEvent::Decoder ftrace_event(field.as_bytes());
+
+      if (ftrace_event.has_sched_switch()) {
+        RedactSwitchEvent(context, std::move(ftrace_event), bundle_message);
+      } else {
+        proto_util::AppendField(field, bundle_message);
+      }
+    }
+  }
+
+  *packet = packet_message.SerializeAsString();
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch.h b/src/trace_redaction/redact_sched_switch.h
new file mode 100644
index 0000000..e33527d
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
+#define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
+
+#include <string>
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+//  Assumptions:
+//    1. This is a hot path (a lot of ftrace packets)
+//    2. Allocations are slower than CPU cycles.
+//
+// Redact sched switch is called "redact" and not "prune" or "scrub" because it
+// is not removing sched switch events, but rather removing information from
+// within the event.
+class RedactSchedSwitch final : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
new file mode 100644
index 0000000..7cb516b
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/base/test/status_matchers.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/redact_sched_switch.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+constexpr std::string_view kTracePath =
+    "test/data/trace-redaction-general.pftrace";
+constexpr std::string_view kPackageName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
+
+class RedactSchedSwitchIntegrationTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.collectors()->emplace_back(new BuildTimeline());
+    redactor_.builders()->emplace_back(new OptimizeTimeline());
+    redactor_.transformers()->emplace_back(new RedactSchedSwitch());
+
+    context_.package_name = kPackageName;
+
+    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
+
+    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+    tmp_dir_.TrackFile("dst.pftrace");
+  }
+
+  base::Status Redact() {
+    return redactor_.Redact(src_trace_, dest_trace_, &context_);
+  }
+
+  base::StatusOr<std::string> LoadOriginal() const {
+    return ReadRawTrace(src_trace_);
+  }
+
+  base::StatusOr<std::string> LoadRedacted() const {
+    return ReadRawTrace(dest_trace_);
+  }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
+    std::string redacted_buffer;
+
+    if (base::ReadFile(path, &redacted_buffer)) {
+      return redacted_buffer;
+    }
+
+    return base::ErrStatus("Failed to read %s", path.c_str());
+  }
+
+  Context context_;
+  TraceRedactor redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+  std::string dest_trace_;
+};
+
+// >>> SELECT uid
+// >>>   FROM package_list
+// >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
+//
+//     +-------+
+//     |  uid  |
+//     +-------+
+//     | 10252 |
+//     +-------+
+//
+// >>> SELECT uid, upid, name
+// >>>   FROM process
+// >>>   WHERE uid=10252
+//
+//     +-------+------+----------------------------------------------+
+//     |  uid  | upid | name                                         |
+//     +-------+------+----------------------------------------------+
+//     | 10252 | 843  | com.Unity.com.unity.multiplayer.samples.coop |
+//     +-------+------+----------------------------------------------+
+//
+// >>> SELECT tid, name
+// >>>   FROM thread
+// >>>   WHERE upid=843 AND name IS NOT NULL
+//
+//     +------+-----------------+
+//     | tid  | name            |
+//     +------+-----------------+
+//     | 7120 | Binder:7105_2   |
+//     | 7127 | UnityMain       |
+//     | 7142 | Job.worker 0    |
+//     | 7143 | Job.worker 1    |
+//     | 7144 | Job.worker 2    |
+//     | 7145 | Job.worker 3    |
+//     | 7146 | Job.worker 4    |
+//     | 7147 | Job.worker 5    |
+//     | 7148 | Job.worker 6    |
+//     | 7150 | Background Job. |
+//     | 7151 | Background Job. |
+//     | 7167 | UnityGfxDeviceW |
+//     | 7172 | AudioTrack      |
+//     | 7174 | FMOD stream thr |
+//     | 7180 | Binder:7105_3   |
+//     | 7184 | UnityChoreograp |
+//     | 7945 | Filter0         |
+//     | 7946 | Filter1         |
+//     | 7947 | Thread-7        |
+//     | 7948 | FMOD mixer thre |
+//     | 7950 | UnityGfxDeviceW |
+//     | 7969 | UnityGfxDeviceW |
+//     +------+-----------------+
+
+TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.c_message();
+
+  auto original = LoadOriginal();
+  ASSERT_OK(original) << original.status().c_message();
+
+  auto redacted = LoadRedacted();
+  ASSERT_OK(redacted) << redacted.status().c_message();
+
+  base::FlatHashMap<int32_t, std::string> expected_names;
+  expected_names.Insert(7120, "Binder:7105_2");
+  expected_names.Insert(7127, "UnityMain");
+  expected_names.Insert(7142, "Job.worker 0");
+  expected_names.Insert(7143, "Job.worker 1");
+  expected_names.Insert(7144, "Job.worker 2");
+  expected_names.Insert(7145, "Job.worker 3");
+  expected_names.Insert(7146, "Job.worker 4");
+  expected_names.Insert(7147, "Job.worker 5");
+  expected_names.Insert(7148, "Job.worker 6");
+  expected_names.Insert(7150, "Background Job.");
+  expected_names.Insert(7151, "Background Job.");
+  expected_names.Insert(7167, "UnityGfxDeviceW");
+  expected_names.Insert(7172, "AudioTrack");
+  expected_names.Insert(7174, "FMOD stream thr");
+  expected_names.Insert(7180, "Binder:7105_3");
+  expected_names.Insert(7184, "UnityChoreograp");
+  expected_names.Insert(7945, "Filter0");
+  expected_names.Insert(7946, "Filter1");
+  expected_names.Insert(7947, "Thread-7");
+  expected_names.Insert(7948, "FMOD mixer thre");
+  expected_names.Insert(7950, "UnityGfxDeviceW");
+  expected_names.Insert(7969, "UnityGfxDeviceW");
+
+  auto redacted_trace_data = LoadRedacted();
+  ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
+
+  protos::pbzero::Trace::Decoder decoder(redacted_trace_data.value());
+
+  for (auto packet = decoder.packet(); packet; ++packet) {
+    protos::pbzero::TracePacket::Decoder packet_decoder(*packet);
+
+    if (!packet_decoder.has_ftrace_events()) {
+      continue;
+    }
+
+    protos::pbzero::FtraceEventBundle::Decoder ftrace_events_decoder(
+        packet_decoder.ftrace_events());
+
+    for (auto event = ftrace_events_decoder.event(); event; ++event) {
+      protos::pbzero::FtraceEvent::Decoder event_decoder(*event);
+
+      if (!event_decoder.has_sched_switch()) {
+        continue;
+      }
+
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_decoder(
+          event_decoder.sched_switch());
+
+      ASSERT_TRUE(sched_decoder.has_next_pid());
+      ASSERT_TRUE(sched_decoder.has_prev_pid());
+
+      auto next_pid = sched_decoder.next_pid();
+      auto prev_pid = sched_decoder.prev_pid();
+
+      // If the pid is expected, make sure it has the right now. If it is not
+      // expected, it should be missing.
+      const auto* next_comm = expected_names.Find(next_pid);
+      const auto* prev_comm = expected_names.Find(prev_pid);
+
+      if (next_comm) {
+        EXPECT_TRUE(sched_decoder.has_next_comm());
+        EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_next_comm());
+      }
+
+      if (prev_comm) {
+        EXPECT_TRUE(sched_decoder.has_prev_comm());
+        EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+      } else {
+        EXPECT_FALSE(sched_decoder.has_prev_comm());
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
new file mode 100644
index 0000000..5f74078
--- /dev/null
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/redact_sched_switch.h"
+#include "protos/perfetto/trace/ftrace/power.gen.h"
+#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/trace.gen.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+constexpr uint64_t kUidA = 1;
+constexpr uint64_t kUidB = 2;
+constexpr uint64_t kUidC = 3;
+
+constexpr int32_t kNoParent = 10;
+constexpr int32_t kPidA = 11;
+constexpr int32_t kPidB = 12;
+constexpr int32_t kPidC = 13;
+
+constexpr std::string_view kCommA = "comm-a";
+constexpr std::string_view kCommB = "comm-b";
+constexpr std::string_view kCommC = "comm-c";
+
+constexpr uint64_t kTimeA = 100;
+constexpr uint64_t kTimeB = 200;
+constexpr uint64_t kTimeC = 300;
+
+}  // namespace
+
+// Tests which nested messages and fields are removed.
+class RedactSchedSwitchTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    context_.timeline = std::make_unique<ProcessThreadTimeline>();
+
+    // Three concurrent processes. No parent. All in different packages.
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(0, kPidC, kNoParent, kUidC));
+
+    context_.timeline->Sort();
+  }
+
+  void BeginBundle() { ftrace_bundle_ = trace_packet_.mutable_ftrace_events(); }
+
+  void AddSwitch(uint64_t ts,
+                 int32_t prev_pid,
+                 std::string_view prev_comm,
+                 int32_t next_pid,
+                 std::string_view next_comm) {
+    ASSERT_NE(ftrace_bundle_, nullptr);
+
+    auto* event = ftrace_bundle_->add_event();
+    event->set_timestamp(ts);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_pid(prev_pid);
+    sched_switch->set_prev_comm(std::string(prev_comm));
+    sched_switch->set_next_pid(next_pid);
+    sched_switch->set_next_comm(std::string(next_comm));
+  }
+
+  base::StatusOr<protos::gen::TracePacket> Transform() {
+    auto packet = trace_packet_.SerializeAsString();
+    auto result = transform_.Transform(context_, &packet);
+
+    if (!result.ok()) {
+      return result;
+    }
+
+    protos::gen::TracePacket redacted_packet;
+    redacted_packet.ParseFromString(packet);
+
+    return redacted_packet;
+  }
+
+  Context context_;
+
+  const RedactSchedSwitch& transform() const { return transform_; }
+
+ private:
+  protos::gen::TracePacket trace_packet_;
+  protos::gen::FtraceEventBundle* ftrace_bundle_;
+
+  RedactSchedSwitch transform_;
+};
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNullPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  ASSERT_FALSE(transform().Transform(context, nullptr).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForEmptyPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  std::string packet_str = "";
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoTimeline) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kUidA;
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, ReturnsErrorForNoPackage) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedSwitchTest, BundleWithNonEventChild) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = 0;
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket packet;
+  auto* events = packet.mutable_ftrace_events();
+  events->set_cpu(0);
+
+  auto* event = events->add_event();
+  event->set_timestamp(kPidA);
+  event->set_pid(kPidA);
+
+  auto* sched_switch = event->mutable_sched_switch();
+  sched_switch->set_prev_comm("swapper/0");
+  sched_switch->set_prev_pid(kPidA);
+  sched_switch->set_prev_prio(120);
+  sched_switch->set_prev_state(0);
+
+  sched_switch->set_next_comm("writer");
+  sched_switch->set_next_pid(kPidB);
+  sched_switch->set_next_prio(96);
+
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket redacted;
+  redacted.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(redacted.has_ftrace_events());
+  ASSERT_TRUE(redacted.ftrace_events().has_cpu());
+}
+
+// There are more than sched_switch events in the ftrace_events message.
+// Beyond supporting simple fields along side the event (e.g. cpu), not all
+// events will contain sched_switch events. Make sure that all every message is
+// retained while redacting the sched_switch.
+TEST_F(RedactSchedSwitchTest, KeepsNonSwitchEvents) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = 2;
+
+  // Keep the previous PID and remove the next PID.
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(0, 0, 1, 2));
+  context.timeline->Sort();
+
+  // packet {
+  //   ftrace_events {
+  //     cpu: 0
+  //     event {
+  //       timestamp: 6702093744766292
+  //       pid: 0
+  //       cpu_idle {
+  //         state: 4294967295
+  //         cpu_id: 0
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744772646
+  //       pid: 0
+  //       sched_switch {
+  //         prev_comm: "swapper/0"
+  //         prev_pid: 0
+  //         prev_prio: 120
+  //         prev_state: 0
+  //         next_comm: "writer"
+  //         next_pid: 23020
+  //         next_prio: 96
+  //       }
+  //     }
+  //     event {
+  //       timestamp: 6702093744803376
+  //       pid: 23020
+  //       sched_waking {
+  //         comm: "FastMixer"
+  //         pid: 1619
+  //         prio: 96
+  //         success: 1
+  //         target_cpu: 1
+  //       }
+  //     }
+  //   }
+  // }
+
+  protos::gen::TracePacket source_packet;
+  source_packet.mutable_ftrace_events()->set_cpu(0);
+
+  // cpu_idle
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744766292);
+    event->set_pid(0);
+
+    auto* cpu_idle = event->mutable_cpu_idle();
+    cpu_idle->set_state(4294967295);
+    cpu_idle->set_cpu_id(0);
+  } while (false);
+
+  // sched_switch
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744772646);
+    event->set_pid(0);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_comm("swapper/0");
+    sched_switch->set_prev_pid(0);
+    sched_switch->set_prev_prio(120);
+    sched_switch->set_prev_state(0);
+    sched_switch->set_next_comm("writer");
+    sched_switch->set_next_pid(23020);
+    sched_switch->set_next_prio(96);
+  } while (false);
+
+  // sched_waking
+  do {
+    auto* event = source_packet.mutable_ftrace_events()->add_event();
+    event->set_timestamp(6702093744803376);
+    event->set_pid(23020);
+
+    auto* sched_waking = event->mutable_sched_waking();
+    sched_waking->set_comm("FastMixer");
+    sched_waking->set_pid(1619);
+    sched_waking->set_prio(96);
+    sched_waking->set_success(1);
+    sched_waking->set_target_cpu(1);
+  } while (false);
+
+  auto packet_str = source_packet.SerializeAsString();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  protos::gen::TracePacket packet;
+  source_packet.ParseFromString(packet_str);
+
+  // Make sure values alongside the "event" value (e.g. "cpu") are retained.
+  ASSERT_TRUE(source_packet.has_ftrace_events());
+
+  auto& ftrace_packets = source_packet.ftrace_events();
+
+  ASSERT_TRUE(ftrace_packets.has_cpu());
+  ASSERT_EQ(ftrace_packets.cpu(), 0u);
+
+  // Assumes order is retained.
+  ASSERT_EQ(ftrace_packets.event_size(), 3);
+  ASSERT_TRUE(ftrace_packets.event().at(0).has_cpu_idle());
+  ASSERT_TRUE(ftrace_packets.event().at(1).has_sched_switch());
+  ASSERT_TRUE(ftrace_packets.event().at(2).has_sched_waking());
+
+  // The sched switch event's next comm should be cleared.
+  const auto& sched_switch = ftrace_packets.event().at(1).sched_switch();
+
+  ASSERT_TRUE(sched_switch.has_prev_comm());
+  ASSERT_EQ(sched_switch.prev_comm(), "swapper/0");
+
+  ASSERT_FALSE(sched_switch.has_next_comm());
+}
+
+class CommTestParams {
+ public:
+  CommTestParams(size_t event_index,
+                 int32_t prev_pid,
+                 std::optional<std::string_view> prev_comm,
+                 int32_t next_pid,
+                 std::optional<std::string_view> next_comm)
+      : event_index_(event_index),
+        prev_pid_(prev_pid),
+        prev_comm_(prev_comm),
+        next_pid_(next_pid),
+        next_comm_(next_comm) {}
+
+  size_t event_index() const { return event_index_; }
+
+  int32_t prev_pid() const { return prev_pid_; }
+
+  std::optional<std::string> prev_comm() const { return prev_comm_; }
+
+  int32_t next_pid() const { return next_pid_; }
+
+  std::optional<std::string> next_comm() const { return next_comm_; }
+
+ private:
+  size_t event_index_;
+
+  int32_t prev_pid_;
+  std::optional<std::string> prev_comm_;
+
+  int32_t next_pid_;
+  std::optional<std::string> next_comm_;
+};
+
+class RedactSchedSwitchTestRemoveComm
+    : public RedactSchedSwitchTest,
+      public testing::WithParamInterface<CommTestParams> {};
+
+TEST_P(RedactSchedSwitchTestRemoveComm, AllEvents) {
+  auto params = GetParam();
+
+  context_.package_uid = kUidA;
+
+  BeginBundle();
+
+  // Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+  AddSwitch(kTimeA, kPidA, kCommA, kPidB, kCommB);
+  AddSwitch(kTimeB, kPidB, kCommB, kPidC, kCommC);
+  AddSwitch(kTimeC, kPidC, kCommC, kPidA, kCommA);
+
+  auto packet = Transform();
+
+  ASSERT_TRUE(packet->has_ftrace_events());
+
+  auto& ftrace_events = packet->ftrace_events().event();
+
+  ASSERT_EQ(ftrace_events.size(), 3u);
+
+  auto event_index = params.event_index();
+
+  ASSERT_TRUE(ftrace_events[event_index].has_sched_switch());
+
+  auto& sched_switch = ftrace_events[event_index].sched_switch();
+
+  ASSERT_EQ(sched_switch.prev_pid(), params.prev_pid());
+  ASSERT_EQ(sched_switch.next_pid(), params.next_pid());
+
+  ASSERT_EQ(sched_switch.has_prev_comm(), params.prev_comm().has_value());
+  ASSERT_EQ(sched_switch.has_next_comm(), params.next_comm().has_value());
+
+  if (sched_switch.has_prev_comm()) {
+    ASSERT_EQ(sched_switch.prev_comm(), params.prev_comm());
+  }
+
+  if (sched_switch.has_next_comm()) {
+    ASSERT_EQ(sched_switch.next_comm(), params.next_comm());
+  }
+}
+
+// Cycle through all the processes: Pid A -> Pid B -> Pid C -> Pid A
+//
+// Only kPidA is attached to kUidA, so it should be the only one with a comm
+// value.
+INSTANTIATE_TEST_SUITE_P(
+    EveryPid,
+    RedactSchedSwitchTestRemoveComm,
+    testing::Values(CommTestParams(0, kPidA, kCommA, kPidB, std::nullopt),
+                    CommTestParams(1, kPidB, std::nullopt, kPidC, std::nullopt),
+                    CommTestParams(2, kPidC, std::nullopt, kPidA, kCommA)));
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_waking.cc b/src/trace_redaction/redact_sched_waking.cc
new file mode 100644
index 0000000..ce5c26d
--- /dev/null
+++ b/src/trace_redaction/redact_sched_waking.cc
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/redact_sched_waking.h"
+
+#include <string>
+
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/proto_util.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+// Redact sched waking trace events in a ftrace event bundle:
+//
+//  event {
+//    timestamp: 6702093787823849
+//    pid: 814
+//    sched_waking {
+//      comm: "surfaceflinger"
+//      pid: 756
+//      prio: 97
+//      success: 1
+//      target_cpu: 2
+//    }
+//  }
+//
+// The three values needed are:
+//
+//  1. event.pid
+//  2. event.timestamp
+//  3. event.sched_waking.pid
+//
+// The two checks that are executed are:
+//
+//  1. package(event.pid).at(event.timestamp).is(target)
+//  2. package(event.sched_waking.pid).at(event.timestamp).is(target)
+//
+// Both must be true in order to keep an event.
+bool KeepEvent(const Context& context, protozero::Field bundle_field) {
+  PERFETTO_DCHECK(context.timeline);
+  PERFETTO_DCHECK(context.package_uid.has_value());
+
+  PERFETTO_DCHECK(bundle_field.valid());
+  PERFETTO_DCHECK(bundle_field.id() ==
+                  protos::pbzero::FtraceEventBundle::kEventFieldNumber);
+
+  protozero::ProtoDecoder event_decoder(bundle_field.as_bytes());
+
+  auto sched_waking = event_decoder.FindField(
+      protos::pbzero::FtraceEvent::kSchedWakingFieldNumber);
+
+  if (!sched_waking.valid()) {
+    return true;
+  }
+
+  auto timestamp = event_decoder.FindField(
+      protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+
+  if (!timestamp.valid()) {
+    return false;
+  }
+
+  auto outer_pid =
+      event_decoder.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
+
+  if (!outer_pid.valid()) {
+    return false;
+  }
+
+  auto outer_slice = context.timeline->Search(
+      timestamp.as_uint64(), static_cast<int32_t>(outer_pid.as_uint32()));
+
+  if (outer_slice.uid != context.package_uid.value()) {
+    return false;
+  }
+
+  protozero::ProtoDecoder waking_decoder(sched_waking.as_bytes());
+
+  auto inner_pid = waking_decoder.FindField(
+      protos::pbzero::SchedWakingFtraceEvent::kPidFieldNumber);
+
+  if (!inner_pid.valid()) {
+    return false;
+  }
+
+  auto inner_slice =
+      context.timeline->Search(timestamp.as_uint64(), inner_pid.as_int32());
+  return inner_slice.uid == context.package_uid.value();
+}
+
+}  // namespace
+
+base::Status RedactSchedWaking::Transform(const Context& context,
+                                          std::string* packet) const {
+  if (packet == nullptr || packet->empty()) {
+    return base::ErrStatus("RedactSchedWaking: null or empty packet.");
+  }
+
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("RedactSchedWaking: missing packet uid.");
+  }
+
+  if (!context.timeline) {
+    return base::ErrStatus("RedactSchedWaking: missing timeline.");
+  }
+
+  protozero::ProtoDecoder packet_decoder(*packet);
+
+  auto trace_event_bundle = packet_decoder.FindField(
+      protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+  if (!trace_event_bundle.valid()) {
+    return base::OkStatus();
+  }
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet_message;
+  packet_message.Reset();
+
+  for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
+       packet_field = packet_decoder.ReadField()) {
+    if (packet_field.id() !=
+        protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      proto_util::AppendField(packet_field, packet_message.get());
+      continue;
+    }
+
+    protozero::ProtoDecoder bundle_decoder(packet_field.as_bytes());
+
+    auto* bundle_message = packet_message->set_ftrace_events();
+
+    for (auto field = bundle_decoder.ReadField(); field.valid();
+         field = bundle_decoder.ReadField()) {
+      if (field.id() != protos::pbzero::FtraceEventBundle::kEventFieldNumber ||
+          KeepEvent(context, field)) {
+        proto_util::AppendField(field, bundle_message);
+      }
+    }
+  }
+
+  *packet = packet_message.SerializeAsString();
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_waking.h b/src/trace_redaction/redact_sched_waking.h
new file mode 100644
index 0000000..adb1ec8
--- /dev/null
+++ b/src/trace_redaction/redact_sched_waking.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_REDACTION_REDACT_SCHED_WAKING_H_
+#define SRC_TRACE_REDACTION_REDACT_SCHED_WAKING_H_
+
+#include <string>
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+class RedactSchedWaking final : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_REDACT_SCHED_WAKING_H_
diff --git a/src/trace_redaction/redact_sched_waking_integrationtest.cc b/src/trace_redaction/redact_sched_waking_integrationtest.cc
new file mode 100644
index 0000000..6b6a990
--- /dev/null
+++ b/src/trace_redaction/redact_sched_waking_integrationtest.cc
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/base/test/status_matchers.h"
+#include "src/base/test/tmp_dir_tree.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/optimize_timeline.h"
+#include "src/trace_redaction/redact_sched_waking.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+constexpr std::string_view kTracePath =
+    "test/data/trace-redaction-general.pftrace";
+constexpr std::string_view kPackageName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
+
+class RedactSchedWakingIntegrationTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    redactor_.collectors()->emplace_back(new FindPackageUid());
+    redactor_.collectors()->emplace_back(new BuildTimeline());
+    redactor_.builders()->emplace_back(new OptimizeTimeline());
+    redactor_.transformers()->emplace_back(new RedactSchedWaking());
+
+    context_.package_name = kPackageName;
+
+    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
+
+    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
+    tmp_dir_.TrackFile("dst.pftrace");
+  }
+
+  base::Status Redact() {
+    return redactor_.Redact(src_trace_, dest_trace_, &context_);
+  }
+
+  base::StatusOr<std::string> LoadOriginal() const {
+    return ReadRawTrace(src_trace_);
+  }
+
+  base::StatusOr<std::string> LoadRedacted() const {
+    return ReadRawTrace(dest_trace_);
+  }
+
+ private:
+  base::StatusOr<std::string> ReadRawTrace(const std::string& path) const {
+    std::string redacted_buffer;
+
+    if (base::ReadFile(path, &redacted_buffer)) {
+      return redacted_buffer;
+    }
+
+    return base::ErrStatus("Failed to read %s", path.c_str());
+  }
+
+  Context context_;
+  TraceRedactor redactor_;
+
+  base::TmpDirTree tmp_dir_;
+
+  std::string src_trace_;
+  std::string dest_trace_;
+};
+
+// >>> SELECT uid
+// >>>   FROM package_list
+// >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
+//
+//     +-------+
+//     |  uid  |
+//     +-------+
+//     | 10252 |
+//     +-------+
+//
+// >>> SELECT uid, upid, name
+// >>>   FROM process
+// >>>   WHERE uid=10252
+//
+//     +-------+------+----------------------------------------------+
+//     |  uid  | upid | name                                         |
+//     +-------+------+----------------------------------------------+
+//     | 10252 | 843  | com.Unity.com.unity.multiplayer.samples.coop |
+//     +-------+------+----------------------------------------------+
+//
+// >>> SELECT tid, name
+// >>>   FROM thread
+// >>>   WHERE upid=843 AND name IS NOT NULL
+//
+//     +------+-----------------+
+//     | tid  | name            |
+//     +------+-----------------+
+//     | 7120 | Binder:7105_2   |
+//     | 7127 | UnityMain       |
+//     | 7142 | Job.worker 0    |
+//     | 7143 | Job.worker 1    |
+//     | 7144 | Job.worker 2    |
+//     | 7145 | Job.worker 3    |
+//     | 7146 | Job.worker 4    |
+//     | 7147 | Job.worker 5    |
+//     | 7148 | Job.worker 6    |
+//     | 7150 | Background Job. |
+//     | 7151 | Background Job. |
+//     | 7167 | UnityGfxDeviceW |
+//     | 7172 | AudioTrack      |
+//     | 7174 | FMOD stream thr |
+//     | 7180 | Binder:7105_3   |
+//     | 7184 | UnityChoreograp |
+//     | 7945 | Filter0         |
+//     | 7946 | Filter1         |
+//     | 7947 | Thread-7        |
+//     | 7948 | FMOD mixer thre |
+//     | 7950 | UnityGfxDeviceW |
+//     | 7969 | UnityGfxDeviceW |
+//     +------+-----------------+
+
+TEST_F(RedactSchedWakingIntegrationTest, OnlyKeepsPackageEvents) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.c_message();
+
+  auto original = LoadOriginal();
+  ASSERT_OK(original) << original.status().c_message();
+
+  auto redacted = LoadRedacted();
+  ASSERT_OK(redacted) << redacted.status().c_message();
+
+  base::FlatHashMap<int32_t, std::string> expected_names;
+  expected_names.Insert(7120, "Binder:7105_2");
+  expected_names.Insert(7127, "UnityMain");
+  expected_names.Insert(7142, "Job.worker 0");
+  expected_names.Insert(7143, "Job.worker 1");
+  expected_names.Insert(7144, "Job.worker 2");
+  expected_names.Insert(7145, "Job.worker 3");
+  expected_names.Insert(7146, "Job.worker 4");
+  expected_names.Insert(7147, "Job.worker 5");
+  expected_names.Insert(7148, "Job.worker 6");
+  expected_names.Insert(7150, "Background Job.");
+  expected_names.Insert(7151, "Background Job.");
+  expected_names.Insert(7167, "UnityGfxDeviceW");
+  expected_names.Insert(7172, "AudioTrack");
+  expected_names.Insert(7174, "FMOD stream thr");
+  expected_names.Insert(7180, "Binder:7105_3");
+  expected_names.Insert(7184, "UnityChoreograp");
+  expected_names.Insert(7945, "Filter0");
+  expected_names.Insert(7946, "Filter1");
+  expected_names.Insert(7947, "Thread-7");
+  expected_names.Insert(7948, "FMOD mixer thre");
+  expected_names.Insert(7950, "UnityGfxDeviceW");
+  expected_names.Insert(7969, "UnityGfxDeviceW");
+
+  auto redacted_trace_data = LoadRedacted();
+  ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
+
+  protos::pbzero::Trace::Decoder decoder(redacted_trace_data.value());
+
+  for (auto packet = decoder.packet(); packet; ++packet) {
+    protos::pbzero::TracePacket::Decoder packet_decoder(*packet);
+
+    if (!packet_decoder.has_ftrace_events()) {
+      continue;
+    }
+
+    protos::pbzero::FtraceEventBundle::Decoder ftrace_events_decoder(
+        packet_decoder.ftrace_events());
+
+    for (auto event = ftrace_events_decoder.event(); event; ++event) {
+      protos::pbzero::FtraceEvent::Decoder event_decoder(*event);
+
+      if (!event_decoder.has_sched_waking()) {
+        continue;
+      }
+
+      ASSERT_TRUE(event_decoder.has_pid());
+      ASSERT_TRUE(
+          expected_names.Find(static_cast<int32_t>(event_decoder.pid())));
+
+      protos::pbzero::SchedWakingFtraceEvent::Decoder waking_decoder(
+          event_decoder.sched_waking());
+
+      ASSERT_TRUE(waking_decoder.has_pid());
+      ASSERT_TRUE(expected_names.Find(waking_decoder.pid()));
+    }
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_waking_unittest.cc b/src/trace_redaction/redact_sched_waking_unittest.cc
new file mode 100644
index 0000000..a196b09
--- /dev/null
+++ b/src/trace_redaction/redact_sched_waking_unittest.cc
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_redaction/redact_sched_waking.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
+#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/trace.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+constexpr int32_t kPackageUid = 1;
+}  // namespace
+
+class RedactSchedWakingTest : public testing::Test {
+ protected:
+  void BeginBundle() { ftrace_bundle_ = trace_packet_.mutable_ftrace_events(); }
+
+  void AddWaking(uint64_t ts, int32_t pid, std::string_view comm) {
+    ASSERT_NE(ftrace_bundle_, nullptr);
+
+    auto* event = ftrace_bundle_->add_event();
+    event->set_timestamp(ts);
+
+    auto* sched_waking = event->mutable_sched_waking();
+    sched_waking->set_pid(pid);
+    sched_waking->set_comm(std::string(comm));
+  }
+
+  const RedactSchedWaking& transform() const { return transform_; }
+
+  // event {
+  //   timestamp: 6702093757720043
+  //   pid: 0
+  //   sched_switch {
+  //     prev_comm: "swapper/0"
+  //     prev_pid: 0
+  //     prev_prio: 120
+  //     prev_state: 0
+  //     next_comm: "Job.worker 5"
+  //     next_pid: 7147
+  //     next_prio: 120
+  //   }
+  // }
+  protos::gen::FtraceEvent* CreateSchedSwitchEvent(
+      protos::gen::FtraceEvent* event) {
+    event->set_timestamp(6702093757720043);
+    event->set_pid(0);
+
+    auto* sched_switch = event->mutable_sched_switch();
+    sched_switch->set_prev_comm("swapper/0");
+    sched_switch->set_prev_pid(0);
+    sched_switch->set_prev_prio(120);
+    sched_switch->set_prev_state(0);
+    sched_switch->set_next_comm("Job.worker 6");
+    sched_switch->set_next_pid(7147);
+    sched_switch->set_next_prio(120);
+
+    return event;
+  }
+
+  // event {
+  //   timestamp: 6702093757727075
+  //   pid: 7147                    <- This pid woke up...
+  //   sched_waking {
+  //     comm: "Job.worker 6"
+  //     pid: 7148                  <- ... this pid
+  //     prio: 120
+  //     success: 1
+  //     target_cpu: 6
+  //   }
+  // }
+  protos::gen::FtraceEvent* CreateSchedWakingEvent(
+      protos::gen::FtraceEvent* event) {
+    event->set_timestamp(6702093757727075);
+    event->set_pid(7147);
+
+    auto* sched_waking = event->mutable_sched_waking();
+    sched_waking->set_comm("Job.worker 6");
+    sched_waking->set_pid(7148);
+    sched_waking->set_prio(120);
+    sched_waking->set_success(1);
+    sched_waking->set_target_cpu(6);
+
+    return event;
+  }
+
+ private:
+  protos::gen::TracePacket trace_packet_;
+  protos::gen::FtraceEventBundle* ftrace_bundle_;
+
+  RedactSchedWaking transform_;
+};
+
+TEST_F(RedactSchedWakingTest, ReturnsErrorForNullPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kPackageUid;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  ASSERT_FALSE(transform().Transform(context, nullptr).ok());
+}
+
+TEST_F(RedactSchedWakingTest, ReturnsErrorForEmptyPacket) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kPackageUid;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  std::string packet_str = "";
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedWakingTest, ReturnsErrorForNoTimeline) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.package_uid = kPackageUid;
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+TEST_F(RedactSchedWakingTest, ReturnsErrorForMissingPackage) {
+  // Don't use context_. These tests will use invalid contexts.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+
+  protos::gen::TracePacket packet;
+  std::string packet_str = packet.SerializeAsString();
+
+  ASSERT_FALSE(transform().Transform(context, &packet_str).ok());
+}
+
+// Assume that the traces has a series of events like the events below. All
+// constants will come from these packets:
+//
+// event {
+//   timestamp: 6702093757720043
+//   pid: 0
+//   sched_switch {
+//     prev_comm: "swapper/0"
+//     prev_pid: 0
+//     prev_prio: 120
+//     prev_state: 0
+//     next_comm: "Job.worker 5"
+//     next_pid: 7147
+//     next_prio: 120
+//   }
+// }
+// event {
+//   timestamp: 6702093757727075
+//   pid: 7147                    <- This pid woke up...
+//   sched_waking {
+//     comm: "Job.worker 6"
+//     pid: 7148                  <- ... this pid
+//     prio: 120
+//     success: 1
+//     target_cpu: 6
+//   }
+// }
+//
+// The waking event is configured to be retained (see
+// KeepsWakingWhenBothPidsConnectToPackage for more information on how). Because
+// this transform only affects waking events, the sched switch event should be
+// retain.
+TEST_F(RedactSchedWakingTest, RetainsNonWakingEvents) {
+  std::string packet_str;
+
+  {
+    protos::gen::TracePacket packet;
+    auto* events = packet.mutable_ftrace_events();
+    events->set_cpu(0);
+
+    CreateSchedSwitchEvent(events->add_event());
+    CreateSchedWakingEvent(events->add_event());
+
+    packet_str = packet.SerializeAsString();
+  }
+
+  // Create a timeline where the wake-target (7147 & 7148) is connected to the
+  // target package.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = kPackageUid;
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7147, 0, kPackageUid));
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7148, 0, kPackageUid));
+  context.timeline->Sort();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  {
+    protos::gen::TracePacket packet;
+    packet.ParseFromString(packet_str);
+
+    ASSERT_TRUE(packet.has_ftrace_events());
+
+    const protos::gen::FtraceEvent* switch_it = nullptr;
+    const protos::gen::FtraceEvent* waking_it = nullptr;
+
+    for (const auto& event : packet.ftrace_events().event()) {
+      if (event.has_sched_switch()) {
+        switch_it = &event;
+      }
+
+      if (event.has_sched_waking()) {
+        waking_it = &event;
+      }
+    }
+
+    // The sched switch event should be here because this primitive should not
+    // affect it.
+    //
+    // The sched waking event should be here because the waker and target
+    // connect to the target package.
+    ASSERT_TRUE(switch_it);
+    ASSERT_TRUE(waking_it);
+  }
+}
+
+// Assume that the traces has a series of events like the events below. All
+// constants will come from these packets:
+//
+// event {
+//   timestamp: 6702093757727075
+//   pid: 7147                    <- This pid woke up...
+//   sched_waking {
+//     comm: "Job.worker 6"
+//     pid: 7148                  <- ... this pid
+//     prio: 120
+//     success: 1
+//     target_cpu: 6
+//   }
+// }
+//
+// Because the sched waking event pid's appears in the timeline and is connected
+// to the target package (kPackageUid), the waking even should remain.
+TEST_F(RedactSchedWakingTest, KeepsWakingWhenBothPidsConnectToPackage) {
+  std::string packet_str;
+
+  {
+    protos::gen::TracePacket packet;
+    auto* events = packet.mutable_ftrace_events();
+    events->set_cpu(0);
+
+    CreateSchedWakingEvent(events->add_event());
+
+    packet_str = packet.SerializeAsString();
+  }
+
+  // Create a timeline where the wake-target (7147 & 7148) is connected to the
+  // target package.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = kPackageUid;
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7147, 0, kPackageUid));
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7148, 0, kPackageUid));
+  context.timeline->Sort();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  {
+    protos::gen::TracePacket packet;
+    packet.ParseFromString(packet_str);
+
+    ASSERT_TRUE(packet.has_ftrace_events());
+
+    const protos::gen::FtraceEvent* waking_it = nullptr;
+
+    for (const auto& event : packet.ftrace_events().event()) {
+      if (event.has_sched_waking()) {
+        waking_it = &event;
+      }
+    }
+
+    ASSERT_TRUE(waking_it);
+
+    const auto& waking = waking_it->sched_waking();
+
+    ASSERT_EQ(waking.comm(), "Job.worker 6");
+    ASSERT_EQ(waking.pid(), 7148);
+    ASSERT_EQ(waking.prio(), 120);
+    ASSERT_EQ(waking.success(), 1);
+    ASSERT_EQ(waking.target_cpu(), 6);
+  }
+}
+
+// Assume that the traces has a series of events like the events below. All
+// constants will come from these packets:
+//
+// event {
+//   timestamp: 6702093757727075
+//   pid: 7147                    <- This pid woke up...
+//   sched_waking {
+//     comm: "Job.worker 6"
+//     pid: 7148                  <- ... this pid
+//     prio: 120
+//     success: 1
+//     target_cpu: 6
+//   }
+// }
+//
+// Because the only one of the sched waking events pid's appears in the
+// timeline and is connected to the target package (kPackageUid), the waking
+// even should remain.
+TEST_F(RedactSchedWakingTest, DropsWakingWhenOnlyWakerPidsConnectToPackage) {
+  std::string packet_str;
+
+  {
+    protos::gen::TracePacket packet;
+    auto* events = packet.mutable_ftrace_events();
+    events->set_cpu(0);
+
+    CreateSchedWakingEvent(events->add_event());
+
+    packet_str = packet.SerializeAsString();
+  }
+
+  // Because 7147 is not added to the timeline, the waking event should not be
+  // retained.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = kPackageUid;
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7148, 0, kPackageUid));
+  context.timeline->Sort();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  {
+    protos::gen::TracePacket packet;
+    packet.ParseFromString(packet_str);
+
+    ASSERT_TRUE(packet.has_ftrace_events());
+
+    const protos::gen::FtraceEvent* waking_it = nullptr;
+
+    for (const auto& event : packet.ftrace_events().event()) {
+      if (event.has_sched_waking()) {
+        waking_it = &event;
+      }
+    }
+
+    ASSERT_FALSE(waking_it);
+  }
+}
+
+// Assume that the traces has a series of events like the events below. All
+// constants will come from these packets:
+//
+// event {
+//   timestamp: 6702093757727075
+//   pid: 7147                    <- This pid woke up...
+//   sched_waking {
+//     comm: "Job.worker 6"
+//     pid: 7148                  <- ... this pid
+//     prio: 120
+//     success: 1
+//     target_cpu: 6
+//   }
+// }
+//
+// Because the only one of the sched waking events pid's appears in the
+// timeline and is connected to the target package (kPackageUid), the waking
+// even should remain.
+TEST_F(RedactSchedWakingTest, DropsWakingWhenOnlyTargetPidsConnectToPackage) {
+  std::string packet_str;
+
+  {
+    protos::gen::TracePacket packet;
+    auto* events = packet.mutable_ftrace_events();
+    events->set_cpu(0);
+
+    CreateSchedWakingEvent(events->add_event());
+
+    packet_str = packet.SerializeAsString();
+  }
+
+  // Because 7147 is not added to the timeline, the waking event should not be
+  // retained.
+  Context context;
+  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context.package_uid = kPackageUid;
+  context.timeline->Append(ProcessThreadTimeline::Event::Open(
+      6702093757720043, 7147, 0, kPackageUid));
+  context.timeline->Sort();
+
+  ASSERT_TRUE(transform().Transform(context, &packet_str).ok());
+
+  {
+    protos::gen::TracePacket packet;
+    packet.ParseFromString(packet_str);
+
+    ASSERT_TRUE(packet.has_ftrace_events());
+
+    const protos::gen::FtraceEvent* waking_it = nullptr;
+
+    for (const auto& event : packet.ftrace_events().event()) {
+      if (event.has_sched_waking()) {
+        waking_it = &event;
+      }
+    }
+
+    ASSERT_FALSE(waking_it);
+  }
+}
+}  // namespace perfetto::trace_redaction
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 221817f..e21997c 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-0d8e9f71b51633ce3927ff1ec94e1ce527c836df8f9fd748924ca46711185331
\ No newline at end of file
+8e2f7043d233187a8e2229e3bfd64c3a21522e52036bf4135641c4b22ff2a40d
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index b7941b6..22796d2 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-bf670ed387e44d0b4209f4da443f15e82be24da5b9573290017775cb5d68446d
\ No newline at end of file
+804eb26f54e147bc536d14354cc1694f748e598b7baaaa50cfd41e137781cbac
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out
new file mode 100644
index 0000000..4f288a7
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_unagg.out
@@ -0,0 +1,362 @@
+android_blocking_calls_unagg {
+  process_with_blocking_calls {
+    process {
+      name: "com.android.systemui"
+      uid: 10001
+      pid: 1000
+    }
+    blocking_calls {
+      name: "binder transaction"
+      cnt: 7
+      total_dur_ms: 20
+      max_dur_ms: 10
+      min_dur_ms: 1
+      total_dur_ns: 20000000
+      max_dur_ns: 10000000
+      min_dur_ns: 1000000
+    }
+    blocking_calls {
+      name: "monitor contention with <...>"
+      cnt: 1
+      total_dur_ms: 12
+      max_dur_ms: 12
+      min_dur_ms: 12
+      total_dur_ns: 12000000
+      max_dur_ns: 12000000
+      min_dur_ns: 12000000
+    }
+    blocking_calls {
+      name: "AIDL::java::IWindowManager::hasNavigationBar::server"
+      cnt: 1
+      total_dur_ms: 10
+      max_dur_ms: 10
+      min_dur_ms: 10
+      total_dur_ns: 10000000
+      max_dur_ns: 10000000
+      min_dur_ns: 10000000
+    }
+  }
+  process_with_blocking_calls {
+   process {
+      name: "com.google.android.apps.nexuslauncher"
+      uid: 10002
+      pid: 2000
+    }
+    blocking_calls {
+      name: "binder transaction"
+      cnt: 6
+      total_dur_ms: 10
+      max_dur_ms: 3
+      min_dur_ms: 1
+      total_dur_ns: 10000000
+      max_dur_ns: 3000000
+      min_dur_ns: 1000000
+   }
+ }
+ process_with_blocking_calls {
+   process {
+     name: "com.google.android.third.process"
+     uid: 10003
+     pid: 3000
+   }
+
+   blocking_calls {
+     name: "CoroutineContinuation"
+     cnt: 2
+     total_dur_ms: 20
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 20000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Contending for pthread mutex"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ExpNotRow#onMeasure(BigTextStyle)"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ExpNotRow#onMeasure(MessagingStyle)"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Garbage Collector"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ImageDecoder#decodeBitmap"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "ImageDecoder#decodeDrawable"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "LoadApkAssetsFd <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on a monitor lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on thread list lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Lock contention on thread suspend count lock <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "NotificationStackScrollLayout#onMeasure"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "SuspendThreadByThreadId <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "animation"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "binder transaction"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "configChanged"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "inflate"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "input"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "layout"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "measure"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "monitor contention with <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "postAndWait"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "relayoutWindow <...>"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "traversal"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+}
+process_with_blocking_calls {
+   process {
+     name: "com.google.android.top.level.slices"
+     uid: 10004
+     pid: 4000
+   }
+
+   blocking_calls {
+     name: "Handler: android.os.AsyncTask"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: android.view.View"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.keyguard.KeyguardUpdateMonitor"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.systemui.broadcast.ActionReceiver"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   blocking_calls {
+     name: "Handler: com.android.systemui.qs.external.TileServiceManager"
+     cnt: 1
+     total_dur_ms: 10
+     max_dur_ms: 10
+     min_dur_ms: 10
+     total_dur_ns: 10000000
+     max_dur_ns: 10000000
+     min_dur_ns: 10000000
+   }
+   }
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index 5038aec..06cbe59 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -121,6 +121,12 @@
         query=Metric('android_blocking_calls_cuj_metric'),
         out=Path('android_blocking_calls_cuj_metric.out'))
 
+  def test_android_blocking_calls_unagg(self):
+    return DiffTestBlueprint(
+        trace=Path('android_blocking_calls_cuj_metric.py'),
+        query=Metric('android_blocking_calls_unagg'),
+        out=Path('android_blocking_calls_unagg.out'))
+
   def test_android_blocking_calls_on_jank_cujs(self):
     return DiffTestBlueprint(
         trace=Path('../graphics/android_jank_cuj.py'),
@@ -296,4 +302,4 @@
            duration_ms: 3878
          }
        }
-       """))
\ No newline at end of file
+       """))
diff --git a/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py b/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
index 48cc56f..37f5d52 100644
--- a/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/graphs/search_tests.py
@@ -154,7 +154,7 @@
         2,"[NULL]"
         """))
 
-  def test_weight_bounded_dfs(self):
+  def test_weight_bounded_dfs_floor(self):
     return DiffTestBlueprint(
         trace=DataPath('counters.json'),
         query="""
@@ -174,7 +174,7 @@
           VALUES (5, 6, 0);
 
           CREATE PERFETTO TABLE roots AS
-          SELECT 0 AS root_node_id, 0 AS root_max_weight
+          SELECT 0 AS root_node_id, 0 AS root_target_weight
           UNION ALL
           VALUES (1, 2)
           UNION ALL
@@ -182,7 +182,7 @@
           UNION ALL
           VALUES (2, 0);
 
-          SELECT * FROM graph_reachable_weight_bounded_dfs!(foo, roots);
+          SELECT * FROM graph_reachable_weight_bounded_dfs!(foo, roots, 1);
         """,
         out=Csv("""
         "root_node_id","node_id","parent_node_id"
@@ -190,8 +190,50 @@
         1,1,"[NULL]"
         1,2,1
         1,3,1
-        1,5,3
-        1,6,5
+        1,4,3
+        3,3,"[NULL]"
+        3,4,3
+        3,5,3
+        3,6,5
+        2,2,"[NULL]"
+        """))
+
+  def test_weight_bounded_dfs_ceiling(self):
+    return DiffTestBlueprint(
+        trace=DataPath('counters.json'),
+        query="""
+          INCLUDE PERFETTO MODULE graphs.search;
+
+          CREATE PERFETTO TABLE foo AS
+          SELECT 0 AS source_node_id, 0 AS dest_node_id, 0 AS edge_weight
+          UNION ALL
+          VALUES (1, 2, 1)
+          UNION ALL
+          VALUES (1, 3, 1)
+          UNION ALL
+          VALUES (3, 4, 1)
+          UNION ALL
+          VALUES (3, 5, 0)
+          UNION ALL
+          VALUES (5, 6, 0);
+
+          CREATE PERFETTO TABLE roots AS
+          SELECT 0 AS root_node_id, 0 AS root_target_weight
+          UNION ALL
+          VALUES (1, 2)
+          UNION ALL
+          VALUES (3, 1)
+          UNION ALL
+          VALUES (2, 0);
+
+          SELECT * FROM graph_reachable_weight_bounded_dfs!(foo, roots, 0);
+        """,
+        out=Csv("""
+        "root_node_id","node_id","parent_node_id"
+        0,0,"[NULL]"
+        1,1,"[NULL]"
+        1,2,1
+        1,3,1
         3,3,"[NULL]"
         3,4,3
         3,5,3
diff --git a/test/trace_processor/diff_tests/stdlib/sched/tests.py b/test/trace_processor/diff_tests/stdlib/sched/tests.py
index bb24f1b..3527185 100644
--- a/test/trace_processor/diff_tests/stdlib/sched/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/sched/tests.py
@@ -13,8 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import Path, DataPath
+from python.generators.diff_tests.testing import Csv
 from python.generators.diff_tests.testing import DiffTestBlueprint
 from python.generators.diff_tests.testing import TestSuite
 
@@ -263,3 +263,29 @@
         "S",3868233011
         "x",35240577
       """))
+
+  def test_sched_previous_runnable_on_thread(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_boot.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.runnable;
+
+        SELECT *
+        FROM sched_previous_runnable_on_thread
+        WHERE prev_wakeup_runnable_id IS NOT NULL
+        ORDER BY id DESC
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "id","prev_runnable_id","prev_wakeup_runnable_id"
+        538199,538191,538191
+        538197,538191,538191
+        538195,538191,538191
+        538190,538136,538136
+        538188,538088,533235
+        538184,538176,524613
+        538181,538178,537492
+        538179,524619,524619
+        538177,537492,537492
+        538175,538174,524613
+        """))
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index 71ca6ab..c2ad002 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -146,34 +146,32 @@
         query="""
         INCLUDE PERFETTO MODULE sched.thread_executing_span;
         SELECT
-          root_id,
-          parent_id,
+          waker_id,
+          prev_id,
+          prev_end_ts,
           id,
           ts,
-          dur,
+          end_ts,
+          is_kernel,
           utid,
-          blocked_dur,
-          blocked_state,
-          blocked_function,
-          is_root,
-          depth
-        FROM _thread_executing_span_graph
-          WHERE blocked_function IS NOT NULL
+          state,
+          blocked_function
+        FROM _wakeup_graph
         ORDER BY ts
         LIMIT 10
         """,
         out=Csv("""
-        "root_id","parent_id","id","ts","dur","utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
-        357,377,380,1735842234188,283571,46,351402620,"I","worker_thread",0,5
-        394,402,405,1735843726296,8545303,46,1208537,"I","worker_thread",0,3
-        357,419,432,1735850643698,16245,95,154087,"I","worker_thread",0,4
-        357,443,446,1735851953029,554638012,95,1103252,"I","worker_thread",0,6
-        357,500,503,1735886367018,191863,46,34095419,"I","worker_thread",0,10
-        357,446,667,1736125372478,52493,46,238813597,"I","worker_thread",0,7
-        357,835,838,1736405409972,278036,46,279985001,"I","worker_thread",0,12
-        357,862,865,1736406817672,7959441,46,1129664,"I","worker_thread",0,10
-        357,882,889,1736413734042,25870,95,7143001,"I","worker_thread",0,11
-        357,882,894,1736413763072,31692550,11,4413060,"I","rcu_gp_fqs_loop",0,11
+        "waker_id","prev_id","prev_end_ts","id","ts","end_ts","is_kernel","utid","state","blocked_function"
+        "[NULL]","[NULL]","[NULL]",5,1735489812571,1735489896509,0,304,"[NULL]","[NULL]"
+        6,"[NULL]","[NULL]",11,1735489876788,1735489953773,0,428,"[NULL]","[NULL]"
+        5,"[NULL]","[NULL]",12,1735489879097,1735490217277,0,243,"[NULL]","[NULL]"
+        11,"[NULL]","[NULL]",17,1735489933912,1735490587658,0,230,"[NULL]","[NULL]"
+        "[NULL]","[NULL]","[NULL]",20,1735489972385,1735489995809,0,298,"[NULL]","[NULL]"
+        "[NULL]",20,1735489995809,25,1735489999987,1735490055966,0,298,"S","[NULL]"
+        25,"[NULL]","[NULL]",28,1735490039439,1735490610238,0,421,"[NULL]","[NULL]"
+        25,"[NULL]","[NULL]",29,1735490042084,1735490068213,0,420,"[NULL]","[NULL]"
+        25,"[NULL]","[NULL]",30,1735490045825,1735491418790,0,1,"[NULL]","[NULL]"
+        17,"[NULL]","[NULL]",41,1735490544063,1735490598211,0,427,"[NULL]","[NULL]"
         """))
 
   def test_thread_executing_span_graph_contains_forked_states(self):
@@ -182,23 +180,15 @@
         query="""
         INCLUDE PERFETTO MODULE sched.thread_executing_span;
         SELECT
-          root_id,
-          parent_id,
           id,
-          ts,
-          dur,
-          utid,
-          blocked_dur,
-          blocked_state,
-          blocked_function,
-          is_root,
-          depth
-        FROM _thread_executing_span_graph
-          WHERE ts = 1735842081507 AND dur = 293868
+          waker_id,
+          prev_id
+        FROM _wakeup_graph
+          WHERE ts = 1735842081507 AND end_ts = 1735842081507 + 293868
         """,
         out=Csv("""
-        "root_id","parent_id","id","ts","dur","utid","blocked_dur","blocked_state","blocked_function","is_root","depth"
-        357,369,376,1735842081507,293868,1465,"[NULL]","[NULL]","[NULL]",0,4
+        "id","waker_id","prev_id"
+        376,369,"[NULL]"
         """))
 
   def test_thread_executing_span_runnable_state_has_no_running(self):
@@ -218,11 +208,11 @@
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
         INCLUDE PERFETTO MODULE sched.thread_executing_span;
-        SELECT ts,dur FROM _thread_executing_span_graph
-          WHERE dur IS NULL OR ts IS NULL
+        SELECT ts,end_ts FROM _wakeup_graph
+          WHERE end_ts IS NULL OR ts IS NULL
         """,
         out=Csv("""
-        "ts","dur"
+        "ts","end_ts"
         """))
 
   def test_thread_executing_span_graph_accepts_null_irq_context(self):
@@ -230,44 +220,222 @@
         trace=DataPath('sched_switch_original.pb'),
         query="""
         INCLUDE PERFETTO MODULE sched.thread_executing_span;
-        SELECT COUNT(*) AS count FROM _thread_executing_span_graph
+        SELECT COUNT(*) AS count FROM _wakeup_graph
         """,
         out=Csv("""
         "count"
-        25
+        17
         """))
 
-  def test_thread_executing_span_critical_path_all(self):
+  def test_thread_executing_span_flatten_critical_path_tasks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_switch_original.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        CREATE PERFETTO TABLE graph AS
+        SELECT
+          id AS source_node_id,
+          COALESCE(waker_id, id) AS dest_node_id,
+          id - COALESCE(waker_id, id) AS edge_weight
+        FROM _wakeup_graph;
+
+        CREATE PERFETTO TABLE roots AS
+        SELECT
+          _wakeup_graph.id AS root_node_id,
+          _wakeup_graph.id - COALESCE(prev_id, _wakeup_graph.id) AS root_target_weight,
+          id,
+          ts,
+          end_ts,
+          utid
+        FROM _wakeup_graph LIMIT 10;
+
+        CREATE PERFETTO TABLE critical_path AS
+        SELECT * FROM graph_reachable_weight_bounded_dfs!(graph, roots, 1);
+
+        SELECT * FROM _flatten_critical_path_tasks!(critical_path);
+        """,
+        out=Csv("""
+        "ts","root_node_id","node_id","dur","node_utid","prev_end_ts"
+        807082868359903,29,29,"[NULL]",8,"[NULL]"
+        807082871734539,35,35,"[NULL]",9,"[NULL]"
+        807082871734539,38,35,45052,9,"[NULL]"
+        807082871779591,38,38,"[NULL]",5,807082871764903
+        807082878623081,45,45,"[NULL]",9,807082871805424
+        807082947156994,57,57,"[NULL]",9,807082878865945
+        807082947246838,62,62,"[NULL]",6,807082879179539
+        807082947261525,63,63,"[NULL]",12,"[NULL]"
+        807082947267463,64,64,"[NULL]",13,"[NULL]"
+        807082947278140,65,65,"[NULL]",14,"[NULL]"
+        807082947288765,66,66,"[NULL]",15,"[NULL]"
+        """))
+
+  def test_thread_executing_span_intervals_to_roots_edge_case(self):
     return DiffTestBlueprint(
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
         INCLUDE PERFETTO MODULE sched.thread_executing_span;
-        SELECT
-          id,
-          ts,
-          dur,
-          utid,
-          critical_path_id,
-          critical_path_blocked_dur,
-          critical_path_blocked_state,
-          critical_path_blocked_function,
-          critical_path_utid INT
-        FROM _thread_executing_span_critical_path(NULL, start_ts, end_ts), trace_bounds
-        ORDER BY ts
-        LIMIT 10
+
+        SELECT * FROM
+        _intervals_to_roots!((SELECT 1477 AS utid, trace_start() AS ts, trace_end() - trace_start() AS dur))
+        LIMIT 10;
         """,
         out=Csv("""
-        "id","ts","dur","utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function","INT"
-        5,1735489812571,83938,304,5,"[NULL]","[NULL]","[NULL]",304
-        6,1735489833977,52463,297,6,"[NULL]","[NULL]","[NULL]",297
-        11,1735489876788,76985,428,11,"[NULL]","[NULL]","[NULL]",428
-        12,1735489879097,338180,243,12,"[NULL]","[NULL]","[NULL]",243
-        17,1735489933912,653746,230,17,"[NULL]","[NULL]","[NULL]",230
-        25,1735489999987,55979,298,25,4178,"S","[NULL]",298
-        28,1735490039439,570799,421,28,"[NULL]","[NULL]","[NULL]",421
-        29,1735490042084,26129,420,29,"[NULL]","[NULL]","[NULL]",420
-        30,1735490045825,1372965,1,30,"[NULL]","[NULL]","[NULL]",1
-        41,1735490544063,54148,427,41,"[NULL]","[NULL]","[NULL]",427
+        "id"
+        11889
+        11892
+        11893
+        11896
+        11897
+        11900
+        11911
+        11916
+        11917
+        11921
+        """))
+
+  def test_thread_executing_span_intervals_to_roots(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_wakeup_trace.atr'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        SELECT * FROM
+        _intervals_to_roots!((SELECT 1477 AS utid, 1737362149192 AS ts, CAST(2e7 AS INT) AS dur))
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "id"
+        11980
+        11983
+        11984
+        11989
+        11990
+        11991
+        11992
+        11993
+        12001
+        12006
+        """))
+
+  def test_thread_executing_span_flatten_critical_paths(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_switch_original.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        CREATE PERFETTO TABLE graph AS
+        SELECT
+          id AS source_node_id,
+          COALESCE(waker_id, id) AS dest_node_id,
+          id - COALESCE(waker_id, id) AS edge_weight
+        FROM _wakeup_graph;
+
+        CREATE PERFETTO TABLE roots AS
+        SELECT
+          _wakeup_graph.id AS root_node_id,
+          _wakeup_graph.id - COALESCE(prev_id, _wakeup_graph.id) AS root_target_weight,
+          id,
+          ts,
+          end_ts,
+          utid
+        FROM _wakeup_graph;
+
+        CREATE PERFETTO TABLE critical_path AS
+        SELECT * FROM graph_reachable_weight_bounded_dfs!(graph, roots, 1);
+
+        SELECT * FROM _flatten_critical_paths!(critical_path, _sleep);
+        """,
+        out=Csv("""
+        "ts","dur","utid","id","root_id","prev_end_ts","critical_path_utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function"
+        807082871764903,14688,9,35,38,"[NULL]",5,38,14688,"S","[NULL]"
+        807082947156994,351302,9,57,76,807082878865945,5,76,68858913,"S","[NULL]"
+        807083031589763,324114,21,127,130,"[NULL]",5,130,80026987,"S","[NULL]"
+        """))
+
+  def test_thread_executing_span_critical_path(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_switch_original.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        CREATE PERFETTO TABLE graph AS
+        SELECT
+          id AS source_node_id,
+          COALESCE(waker_id, id) AS dest_node_id,
+          id - COALESCE(waker_id, id) AS edge_weight
+        FROM _wakeup_graph;
+
+        CREATE PERFETTO TABLE roots AS
+        SELECT
+          _wakeup_graph.id AS root_node_id,
+          _wakeup_graph.id - COALESCE(prev_id, _wakeup_graph.id) AS root_target_weight,
+          id,
+          ts,
+          end_ts,
+          utid
+        FROM _wakeup_graph;
+
+        SELECT * FROM _critical_path!(graph, roots, _sleep);
+        """,
+        out=Csv("""
+        "ts","dur","root_id","id","utid","critical_path_utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function"
+        807082868359903,81302,29,29,8,8,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082871734539,70885,35,35,9,9,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082871764903,14688,38,35,9,5,38,14688,"S","[NULL]"
+        807082871779591,55729,38,38,5,5,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082878623081,242864,45,45,9,9,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947156994,436354,57,57,9,9,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947246838,1038854,62,62,6,6,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947261525,293594,63,63,12,12,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947267463,228958,64,64,13,13,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947278140,54114,65,65,14,14,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947288765,338802,66,66,15,15,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947294182,296875,67,67,16,16,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082947156994,351302,76,57,9,5,76,68858913,"S","[NULL]"
+        807082947508296,122083,76,76,5,5,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082951822463,104427,96,96,9,9,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807082959173506,215104,107,107,6,6,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807083031589763,436198,127,127,21,21,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807083031589763,324114,130,127,21,5,130,80026987,"S","[NULL]"
+        807083031913877,166302,130,130,5,5,"[NULL]","[NULL]","[NULL]","[NULL]"
+        807083032278825,208490,135,135,2,2,"[NULL]","[NULL]","[NULL]","[NULL]"
+        """))
+
+  def test_thread_executing_span_critical_path_by_roots(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_switch_original.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        SELECT * FROM _critical_path_by_roots!(_intervals_to_roots!((SELECT 6 AS utid, trace_start() AS ts, trace_end() - trace_start() AS dur)));
+        """,
+        out=Csv("""
+        "id","ts","dur","utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function","critical_path_utid"
+        62,807082947246838,1038854,6,"[NULL]","[NULL]","[NULL]","[NULL]",6
+        63,807082947261525,293594,12,"[NULL]","[NULL]","[NULL]","[NULL]",12
+        64,807082947267463,228958,13,"[NULL]","[NULL]","[NULL]","[NULL]",13
+        65,807082947278140,54114,14,"[NULL]","[NULL]","[NULL]","[NULL]",14
+        66,807082947288765,338802,15,"[NULL]","[NULL]","[NULL]","[NULL]",15
+        67,807082947294182,296875,16,"[NULL]","[NULL]","[NULL]","[NULL]",16
+        57,807082947156994,351302,9,76,68858913,"S","[NULL]",5
+        76,807082947508296,122083,5,"[NULL]","[NULL]","[NULL]","[NULL]",5
+        96,807082951822463,104427,9,"[NULL]","[NULL]","[NULL]","[NULL]",9
+        107,807082959173506,215104,6,"[NULL]","[NULL]","[NULL]","[NULL]",6
+        """))
+
+  def test_thread_executing_span_critical_path_by_intervals(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sched_switch_original.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+
+        SELECT * FROM _critical_path_by_intervals!((SELECT 6 AS utid, trace_start() AS ts, trace_end() - trace_start() AS dur));
+        """,
+        out=Csv("""
+        "id","ts","dur","utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function","critical_path_utid"
+        62,807082947246838,1038854,6,"[NULL]","[NULL]","[NULL]","[NULL]",6
+        107,807082959173506,215104,6,"[NULL]","[NULL]","[NULL]","[NULL]",6
         """))
 
   def test_thread_executing_span_critical_path_utid(self):
@@ -284,22 +452,22 @@
           critical_path_blocked_dur,
           critical_path_blocked_state,
           critical_path_blocked_function,
-          critical_path_utid INT
+          critical_path_utid
         FROM _thread_executing_span_critical_path((select utid from thread where tid = 3487), start_ts, end_ts), trace_bounds
         ORDER BY ts
         LIMIT 10
         """,
         out=Csv("""
-        "id","ts","dur","utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function","INT"
-        11889,1737349401439,7705561,1477,11889,"[NULL]","[NULL]","[NULL]",1477
+        "id","ts","dur","utid","critical_path_id","critical_path_blocked_dur","critical_path_blocked_state","critical_path_blocked_function","critical_path_utid"
+        11889,1737349401439,7705561,1477,"[NULL]","[NULL]","[NULL]","[NULL]",1477
         11952,1737357107000,547583,1480,11980,547583,"S","[NULL]",1477
-        11980,1737357654583,8430762,1477,11980,547583,"S","[NULL]",1477
+        11980,1737357654583,8430762,1477,"[NULL]","[NULL]","[NULL]","[NULL]",1477
         12052,1737366085345,50400,91,12057,50400,"S","[NULL]",1477
-        12057,1737366135745,6635927,1477,12057,50400,"S","[NULL]",1477
+        12057,1737366135745,6635927,1477,"[NULL]","[NULL]","[NULL]","[NULL]",1477
         12081,1737372771672,12798314,1488,12254,12798314,"S","[NULL]",1477
-        12254,1737385569986,21830622,1477,12254,12798314,"S","[NULL]",1477
+        12254,1737385569986,21830622,1477,"[NULL]","[NULL]","[NULL]","[NULL]",1477
         12517,1737407400608,241267,91,12521,241267,"S","[NULL]",1477
-        12521,1737407641875,1830015,1477,12521,241267,"S","[NULL]",1477
+        12521,1737407641875,1830015,1477,"[NULL]","[NULL]","[NULL]","[NULL]",1477
         12669,1737409471890,68590,91,12672,68590,"S","[NULL]",1477
         """))
 
@@ -307,7 +475,7 @@
     return DiffTestBlueprint(
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
-        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+        INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
         SELECT
           id,
           ts,
@@ -340,7 +508,7 @@
     return DiffTestBlueprint(
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
-        INCLUDE PERFETTO MODULE sched.thread_executing_span;
+        INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
         SELECT HEX(pprof) FROM _thread_executing_span_critical_path_graph("critical path", (select utid from thread where tid = 3487), 1737488133487, 16000), trace_bounds
       """,
         out=BinaryProto(
diff --git a/tools/check_sql_metrics.py b/tools/check_sql_metrics.py
index 3819c27..f025f47 100755
--- a/tools/check_sql_metrics.py
+++ b/tools/check_sql_metrics.py
@@ -41,6 +41,11 @@
         'android_cujs', 'relevant_binder_calls_with_names',
         'android_blocking_calls_cuj_calls'
     ],
+    ('/android'
+    '/android_blocking_calls_unagg.sql'): [
+        'filtered_processes_with_non_zero_blocking_calls',
+        'process_info', 'android_blocking_calls_unagg_calls'
+    ],
     '/android/jank/cujs.sql': ['android_jank_cuj'],
     '/chrome/gesture_flow_event.sql': [
         '{{prefix}}_latency_info_flow_step_filtered'
diff --git a/tools/gen_bazel b/tools/gen_bazel
index 14775fc..049cf40 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -181,6 +181,11 @@
   "PERFETTO_SHLIB_SDK_IMPLEMENTATION"
 ]
 
+# Targets with the "avoid_dep" tag;
+avoid_dep_targets = set([
+  '//python:trace_processor_py_no_resolvers',
+])
+
 # Filter defines that appear in the bazel build file to only those that bazel requires.
 def filter_defines(defines):
   return [d for d in defines if d in bazel_required_defines]
@@ -240,6 +245,8 @@
     self.populate_python_deps(target, label)
     if target.name in public_python_targets:
       label.visibility = ['//visibility:public']
+    if target.name in avoid_dep_targets:
+      label.tags += ['avoid_dep']
     return [label]
 
   def gen_python_binary(self, target: GnParser.Target):
@@ -346,6 +353,7 @@
     self.python_version: Optional[str] = None
     self.root_dir: Optional[str] = None
     self.namespace: Optional[str] = None
+    self.tags: List[str] = []
 
   def __lt__(self, other):
     if isinstance(other, self.__class__):
diff --git a/ui/src/base/async_limiter.ts b/ui/src/base/async_limiter.ts
new file mode 100644
index 0000000..3130d6d
--- /dev/null
+++ b/ui/src/base/async_limiter.ts
@@ -0,0 +1,73 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Deferred, defer} from './deferred';
+
+type Callback = () => Promise<void>;
+
+interface Task {
+  deferred: Deferred<void>;
+  work: Callback;
+}
+
+/**
+ * A tiny task queue management utility that ensures async tasks are not
+ * executed concurrently.
+ *
+ * If a task is run while a previous one is still running, it is enqueued and
+ * run after the first task completes.
+ *
+ * If multiple tasks are enqueued, only the latest task is run.
+ */
+export class AsyncLimiter {
+  private readonly taskQueue: Task[] = [];
+  private isRunning: boolean = false;
+
+  /**
+   * Schedule a task to be run.
+   *
+   * @param work An async function to schedule.
+   * @returns A promise that resolves when either the task has finished
+   * executing, or after the task has silently been discarded because a newer
+   * task was scheduled.
+   */
+  schedule(work: Callback): Promise<void> {
+    const deferred = defer<void>();
+    this.taskQueue.push({work, deferred});
+
+    if (!this.isRunning) {
+      this.isRunning = true;
+      this.runTaskQueue().finally(() => (this.isRunning = false));
+    }
+
+    return deferred;
+  }
+
+  private async runTaskQueue(): Promise<void> {
+    let task: Task | undefined;
+
+    while ((task = this.taskQueue.shift())) {
+      if (this.taskQueue.length > 0) {
+        task.deferred.resolve();
+      } else {
+        try {
+          await task.work();
+          task.deferred.resolve();
+        } catch (e) {
+          task.deferred.reject(e);
+        }
+      }
+    }
+  }
+}
diff --git a/ui/src/base/async_limiter_unittest.ts b/ui/src/base/async_limiter_unittest.ts
new file mode 100644
index 0000000..6b080e1
--- /dev/null
+++ b/ui/src/base/async_limiter_unittest.ts
@@ -0,0 +1,80 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {AsyncLimiter} from './async_limiter';
+
+test('no concurrent callbacks', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+  expect(mock1).toHaveBeenCalled();
+
+  const mock2 = jest.fn();
+  limiter.schedule(async () => mock2());
+  expect(mock2).not.toHaveBeenCalled();
+});
+
+test('queueing', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+
+  const mock2 = jest.fn();
+  await limiter.schedule(async () => mock2());
+
+  expect(mock1).toHaveBeenCalled();
+  expect(mock2).toHaveBeenCalled();
+});
+
+test('multiple queuing', async () => {
+  const limiter = new AsyncLimiter();
+
+  const mock1 = jest.fn();
+  limiter.schedule(async () => mock1());
+
+  const mock2 = jest.fn();
+  limiter.schedule(async () => mock2());
+
+  const mock3 = jest.fn();
+  await limiter.schedule(async () => mock3());
+
+  expect(mock1).toHaveBeenCalled();
+  expect(mock2).not.toHaveBeenCalled();
+  expect(mock3).toHaveBeenCalled();
+});
+
+test('error in callback bubbles up to caller', async () => {
+  const limiter = new AsyncLimiter();
+  const failingCallback = async () => {
+    throw Error();
+  };
+
+  expect(async () => await limiter.schedule(failingCallback)).rejects.toThrow();
+});
+
+test('chain continues even when one callback fails', async () => {
+  const limiter = new AsyncLimiter();
+
+  const failingCallback = async () => {
+    throw Error();
+  };
+  limiter.schedule(failingCallback).catch(() => {});
+
+  const mock = jest.fn();
+  await limiter.schedule(async () => mock());
+
+  expect(mock).toHaveBeenCalled();
+});
diff --git a/ui/src/base/logging.ts b/ui/src/base/logging.ts
index 10fbbff..74cbe44 100644
--- a/ui/src/base/logging.ts
+++ b/ui/src/base/logging.ts
@@ -60,7 +60,7 @@
 
   if (err instanceof ErrorEvent) {
     errType = 'ERROR';
-    errMsg = err.message;
+    errMsg = `${err.error}`;
     errorObj = err.error;
   } else if (err instanceof PromiseRejectionEvent) {
     errType = 'PROMISE_REJ';
diff --git a/ui/src/base/monitor.ts b/ui/src/base/monitor.ts
new file mode 100644
index 0000000..d4e0f87
--- /dev/null
+++ b/ui/src/base/monitor.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+type Reducer = () => unknown;
+type Callback = () => void;
+
+/**
+ * A little helper that monitors a list of immutable objects and calls a
+ * callback only when at least one them changes.
+ */
+export class Monitor {
+  private cached: unknown[];
+
+  constructor(private reducers: Reducer[]) {
+    this.cached = reducers.map(() => undefined);
+  }
+
+  ifStateChanged(callback: Callback): void {
+    const state = this.reducers.map((f) => f());
+    if (state.some((x, i) => x !== this.cached[i])) {
+      callback();
+    }
+    this.cached = state;
+  }
+}
diff --git a/ui/src/base/monitor_unittest.ts b/ui/src/base/monitor_unittest.ts
new file mode 100644
index 0000000..51b22da
--- /dev/null
+++ b/ui/src/base/monitor_unittest.ts
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Monitor} from './monitor';
+
+test('callback is called when state changes', () => {
+  const reducer = jest.fn().mockReturnValue('foo');
+  const monitor = new Monitor([reducer]);
+  const mockCallback = jest.fn();
+
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).toHaveBeenCalledTimes(1);
+
+  mockCallback.mockReset();
+  monitor.ifStateChanged(mockCallback);
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).not.toHaveBeenCalled();
+
+  mockCallback.mockReset();
+  reducer.mockReturnValue('bar');
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).toHaveBeenCalledTimes(1);
+
+  mockCallback.mockReset();
+  monitor.ifStateChanged(mockCallback);
+  expect(mockCallback).not.toHaveBeenCalled();
+});
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index baba17e..4f16d80 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -80,6 +80,7 @@
   trackSortKey: TrackSortKey;
   trackGroup?: string;
   params?: unknown;
+  closeable?: boolean;
 }
 
 export interface PostedTrace {
@@ -211,6 +212,7 @@
         labels: track.labels,
         uri: track.uri,
         params: track.params,
+        closeable: track.closeable,
       };
       if (track.trackGroup === SCROLLING_TRACK_GROUP) {
         state.scrollingTracks.push(trackKey);
@@ -726,6 +728,7 @@
       type: args.type,
       viewingOption: args.viewingOption,
       focusRegex: '',
+      expandedCallsiteByViewingOption: {},
     };
   },
 
@@ -746,10 +749,15 @@
 
   expandFlamegraphState(
     state: StateDraft,
-    args: {expandedCallsite?: CallsiteInfo},
+    args: {
+      expandedCallsite?: CallsiteInfo;
+      viewingOption: FlamegraphStateViewingOption;
+    },
   ): void {
     if (state.currentFlamegraphState === null) return;
-    state.currentFlamegraphState.expandedCallsite = args.expandedCallsite;
+    state.currentFlamegraphState.expandedCallsiteByViewingOption[
+      args.viewingOption
+    ] = args.expandedCallsite;
   },
 
   changeViewFlamegraphState(
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index 63689ca..0817ebf 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {featureFlags} from '../core/feature_flags';
 import {CallsiteInfo, FlamegraphStateViewingOption, ProfileType} from './state';
 
 interface ViewingOption {
@@ -19,6 +20,13 @@
   name: string;
 }
 
+const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({
+  id: 'showHeapGraphDominatorTree',
+  name: 'Show heap graph dominator tree',
+  description: 'Show dominated size and objects tabs in Java heap graph view.',
+  defaultValue: false,
+});
+
 export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
   switch (profileType) {
     case ProfileType.PERF_SAMPLE:
@@ -39,7 +47,22 @@
           option: FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
           name: 'Objects',
         },
-      ];
+      ].concat(
+        SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
+          ? [
+              {
+                option:
+                  FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
+                name: 'Dominated size',
+              },
+              {
+                option:
+                  FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
+                name: 'Dominated objects',
+              },
+            ]
+          : [],
+      );
     case ProfileType.HEAP_PROFILE:
       return [
         {
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 19b156c..92efee5 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -22,7 +22,7 @@
 } from '../frontend/pivot_table_types';
 import {PrimaryTrackSortKey} from '../public/index';
 
-import {Direction} from './event_set';
+import {Direction} from '../core/event_set';
 
 import {
   selectionToLegacySelection,
@@ -147,7 +147,8 @@
 // 48. Rename legacySelection -> selection and introduce new Selection type.
 // 49. Remove currentTab, which is only relevant to TabsV1.
 // 50. Remove ftrace filter state.
-export const STATE_VERSION = 50;
+// 51. Changed structure of FlamegraphState.expandedCallsiteByViewingOption.
+export const STATE_VERSION = 51;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -189,6 +190,24 @@
   OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS',
   OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS',
   PERF_SAMPLES_KEY = 'PERF_SAMPLES',
+  DOMINATOR_TREE_OBJ_SIZE_KEY = 'DOMINATED_OBJ_SIZE',
+  DOMINATOR_TREE_OBJ_COUNT_KEY = 'DOMINATED_OBJ_COUNT',
+}
+
+const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [
+  FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
+  FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
+] as const;
+
+export type HeapGraphDominatorTreeViewingOption =
+  (typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number];
+
+export function isHeapGraphDominatorTreeViewingOption(
+  option: FlamegraphStateViewingOption,
+): option is HeapGraphDominatorTreeViewingOption {
+  return (
+    HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphStateViewingOption[]
+  ).includes(option);
 }
 
 export interface FlamegraphState {
@@ -199,7 +218,7 @@
   type: ProfileType;
   viewingOption: FlamegraphStateViewingOption;
   focusRegex: string;
-  expandedCallsite?: CallsiteInfo;
+  expandedCallsiteByViewingOption: {[key: string]: CallsiteInfo | undefined};
 }
 
 export interface CallsiteInfo {
@@ -262,6 +281,7 @@
   trackGroup?: string;
   params?: unknown;
   state?: unknown;
+  closeable?: boolean;
 }
 
 export interface TrackGroupState {
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index aa123f7..a10252c 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Duration, time} from '../base/time';
+import {Duration, Time, time} from '../base/time';
 import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {
@@ -26,6 +26,7 @@
   FlamegraphState,
   FlamegraphStateViewingOption,
   ProfileType,
+  isHeapGraphDominatorTreeViewingOption,
 } from '../common/state';
 import {FlamegraphDetails, globals} from '../frontend/globals';
 import {publishFlamegraphDetails} from '../frontend/publish';
@@ -115,6 +116,10 @@
   private flamegraphDetails: FlamegraphDetails = {};
   private areaSelectionHandler: AreaSelectionHandler;
   private cache: TablesCache;
+  private heapGraphSelected: {upid: number; timestamp: time} = {
+    upid: -1,
+    timestamp: Time.INVALID,
+  };
 
   constructor(private args: FlamegraphControllerArgs) {
     super('main');
@@ -197,13 +202,12 @@
 
     this.lastSelectedFlamegraphState = {...selection};
 
-    const expandedId = selectedFlamegraphState.expandedCallsite
-      ? selectedFlamegraphState.expandedCallsite.id
-      : -1;
-    const rootSize =
-      selectedFlamegraphState.expandedCallsite === undefined
-        ? undefined
-        : selectedFlamegraphState.expandedCallsite.totalSize;
+    const expandedCallsite =
+      selectedFlamegraphState.expandedCallsiteByViewingOption[
+        selectedFlamegraphState.viewingOption
+      ];
+    const expandedId = expandedCallsite ? expandedCallsite.id : -1;
+    const rootSize = expandedCallsite?.totalSize;
 
     const key = `${selectedFlamegraphState.upids};${selectedFlamegraphState.start};${selectedFlamegraphState.end}`;
 
@@ -238,7 +242,7 @@
           this.lastSelectedFlamegraphState.viewingOption,
           isInAreaSelection,
           rootSize,
-          this.lastSelectedFlamegraphState.expandedCallsite,
+          expandedCallsite,
         );
       }
     } finally {
@@ -264,8 +268,10 @@
         this.lastSelectedFlamegraphState.viewingOption !==
           selection.viewingOption ||
         this.lastSelectedFlamegraphState.focusRegex !== selection.focusRegex ||
-        this.lastSelectedFlamegraphState.expandedCallsite !==
-          selection.expandedCallsite)
+        this.lastSelectedFlamegraphState.expandedCallsiteByViewingOption[
+          selection.viewingOption
+        ] !==
+          selection.expandedCallsiteByViewingOption[selection.viewingOption])
     );
   }
 
@@ -311,7 +317,7 @@
     if (this.flamegraphDatasets.has(key)) {
       currentData = this.flamegraphDatasets.get(key)!;
     } else {
-      // TODO(hjd): Show loading state.
+      // TODO(b/330703412): Show loading state.
 
       // Collecting data for drawing flamegraph for selected profile.
       // Data needs to be in following format:
@@ -322,6 +328,7 @@
         upids,
         type,
         focusRegex,
+        viewingOption,
       );
       currentData = await this.getFlamegraphDataFromTables(
         tableName,
@@ -374,6 +381,18 @@
         totalColumnName = 'cumulativeSize';
         selfColumnName = 'size';
         break;
+      case FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
+        orderBy = `where depth < ${maxDepth} order by depth,
+          cumulativeCount desc, name`;
+        totalColumnName = 'cumulativeCount';
+        selfColumnName = 'count';
+        break;
+      case FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
+        orderBy = `where depth < ${maxDepth} order by depth,
+          cumulativeSize desc, name`;
+        totalColumnName = 'cumulativeSize';
+        selfColumnName = 'size';
+        break;
       default:
         const exhaustiveCheck: never = viewingOption;
         throw new Error(`Unhandled case: ${exhaustiveCheck}`);
@@ -381,21 +400,21 @@
     }
 
     const callsites = await this.args.engine.query(`
-        SELECT
-        id as hash,
-        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
-        IFNULL(parent_id, -1) as parentHash,
-        depth,
-        cumulative_size as cumulativeSize,
-        cumulative_alloc_size as cumulativeAllocSize,
-        cumulative_count as cumulativeCount,
-        cumulative_alloc_count as cumulativeAllocCount,
-        map_name as mapping,
-        size,
-        count,
-        IFNULL(source_file, '') as sourceFile,
-        IFNULL(line_number, -1) as lineNumber
-        from ${tableName} ${orderBy}`);
+      SELECT
+      id as hash,
+      IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
+      IFNULL(parent_id, -1) as parentHash,
+      depth,
+      cumulative_size as cumulativeSize,
+      cumulative_alloc_size as cumulativeAllocSize,
+      cumulative_count as cumulativeCount,
+      cumulative_alloc_count as cumulativeAllocCount,
+      map_name as mapping,
+      size,
+      count,
+      IFNULL(source_file, '') as sourceFile,
+      IFNULL(line_number, -1) as lineNumber
+      from ${tableName} ${orderBy}`);
 
     const flamegraphData: CallsiteInfo[] = [];
     const hashToindex: Map<number, number> = new Map();
@@ -467,6 +486,7 @@
     upids: number[],
     type: ProfileType,
     focusRegex: string,
+    viewingOption: FlamegraphStateViewingOption,
   ): Promise<string> {
     const flamegraphType = getFlamegraphType(type);
     if (type === ProfileType.PERF_SAMPLE) {
@@ -493,6 +513,14 @@
           )`,
       );
     }
+    if (
+      type === ProfileType.JAVA_HEAP_GRAPH &&
+      isHeapGraphDominatorTreeViewingOption(viewingOption)
+    ) {
+      return this.cache.getTableName(
+        await this.loadHeapGraphDominatorTreeQuery(upids[0], end),
+      );
+    }
     return this.cache.getTableName(
       `select id, name, map_name, parent_id, depth, cumulative_size,
           cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
@@ -508,6 +536,91 @@
     );
   }
 
+  private async loadHeapGraphDominatorTreeQuery(upid: number, timestamp: time) {
+    const selectTreeQuery = `
+    -- cache invalidate: upid ${upid}, ts ${timestamp}
+    SELECT * FROM heap_graph_type_dominated`;
+    if (
+      this.heapGraphSelected.upid === upid &&
+      this.heapGraphSelected.timestamp === timestamp
+    ) {
+      return selectTreeQuery;
+    }
+    this.heapGraphSelected = {upid, timestamp};
+    this.args.engine.query(`
+    INCLUDE PERFETTO MODULE memory.heap_graph_dominator_tree;
+
+    -- heap graph dominator tree with objects as nodes and all relavant
+    -- object self stats and dominated stats
+    CREATE PERFETTO TABLE heap_graph_object_dominated AS
+    SELECT
+     node.id,
+     node.idom_id,
+     node.dominated_obj_count,
+     node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
+     node.depth,
+     obj.type_id,
+     obj.root_type,
+     obj.self_size + obj.native_size AS self_size
+    FROM memory_heap_graph_dominator_tree node
+    JOIN heap_graph_object obj USING(id)
+    WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
+    -- required to accelerate the recursive cte below
+    ORDER BY idom_id;
+
+    -- calculate for each object node in the dominator tree the
+    -- HASH(path of type_id's from the super root to the object)
+    CREATE PERFETTO TABLE _dominator_tree_path_hash AS
+    WITH RECURSIVE _tree_visitor(id, path, path_hash) AS (
+      SELECT
+        id,
+        CAST(type_id AS text) || '-' || IFNULL(root_type, '') AS path,
+        HASH(
+          CAST(type_id AS text) || '-' || IFNULL(root_type, '')
+        ) AS path_hash
+      FROM heap_graph_object_dominated
+      WHERE depth = 1
+      UNION ALL
+      SELECT
+        child.id,
+        parent.path || '/' || CAST(type_id AS text) AS path,
+        HASH(parent.path || '/' || CAST(type_id AS text)) AS path_hash
+      FROM heap_graph_object_dominated child
+      JOIN _tree_visitor parent ON child.idom_id = parent.id
+    )
+    SELECT * from _tree_visitor
+    ORDER BY id;
+
+    -- merge object nodes with the same path into one "class type node", so the
+    -- end result is a tree where nodes are identified by their types and the
+    -- dominator relationships are preserved.
+    CREATE PERFETTO TABLE heap_graph_type_dominated AS
+    SELECT
+      map.path_hash as id,
+      COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
+        node.root_type IS NOT NULL,
+        ' [' || node.root_type || ']', ''
+      ) AS name,
+      IFNULL(parent_map.path_hash, -1) AS parent_id,
+      node.depth - 1 AS depth,
+      sum(dominated_size) AS cumulative_size,
+      -1 AS cumulative_alloc_size,
+      sum(dominated_obj_count) AS cumulative_count,
+      -1 AS cumulative_alloc_count,
+      '' as map_name,
+      '' as source_file,
+      -1 as line_number,
+      sum(self_size) AS size,
+      count(*) AS count
+    FROM heap_graph_object_dominated node
+    JOIN _dominator_tree_path_hash map USING(id)
+    LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
+    JOIN heap_graph_class cls ON node.type_id = cls.id
+    GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;`);
+
+    return selectTreeQuery;
+  }
+
   getMinSizeDisplayed(
     flamegraphData: CallsiteInfo[],
     rootSize?: number,
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 9e39eed..161fba7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -104,7 +104,6 @@
 type States = 'init' | 'loading_trace' | 'ready';
 
 const METRICS = [
-  'android_startup',
   'android_ion',
   'android_lmk',
   'android_dma_heap',
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 8a03d47..583e6bc 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -28,6 +28,7 @@
   'dev.perfetto.AndroidNetwork',
   'dev.perfetto.AndroidPerf',
   'dev.perfetto.AndroidPerfTraceCounters',
+  'dev.perfetto.AndroidStartup',
   'dev.perfetto.BookmarkletApi',
   'dev.perfetto.CoreCommands',
   'dev.perfetto.LargeScreensPerf',
diff --git a/ui/src/common/event_set.ts b/ui/src/core/event_set.ts
similarity index 100%
rename from ui/src/common/event_set.ts
rename to ui/src/core/event_set.ts
diff --git a/ui/src/common/event_set_nocompile_test.ts b/ui/src/core/event_set_nocompile_test.ts
similarity index 100%
rename from ui/src/common/event_set_nocompile_test.ts
rename to ui/src/core/event_set_nocompile_test.ts
diff --git a/ui/src/common/event_set_unittest.ts b/ui/src/core/event_set_unittest.ts
similarity index 100%
rename from ui/src/common/event_set_unittest.ts
rename to ui/src/core/event_set_unittest.ts
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 940ff31..4bed3fc 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -322,7 +322,7 @@
 
         if (engine !== undefined && trackUtid != 0) {
           await runQuery(
-            `SELECT IMPORT('sched.thread_executing_span');`,
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
             engine,
           );
           await addDebugSliceTrack(
@@ -365,7 +365,7 @@
 
         if (engine !== undefined && trackUtid != 0) {
           await runQuery(
-            `SELECT IMPORT('sched.thread_executing_span');`,
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
             engine,
           );
           await addDebugSliceTrack(
@@ -399,7 +399,7 @@
 
         if (engine !== undefined && trackUtid != 0) {
           addQueryResultsTab({
-            query: `SELECT IMPORT('sched.thread_executing_span');
+            query: `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
                    SELECT *
                       FROM
                         _thread_executing_span_critical_path_graph(
diff --git a/ui/src/frontend/debug_tracks.ts b/ui/src/frontend/debug_tracks.ts
index 7e2ce7e..6ff6297 100644
--- a/ui/src/frontend/debug_tracks.ts
+++ b/ui/src/frontend/debug_tracks.ts
@@ -67,7 +67,6 @@
   const trackConfig: DebugTrackV2Config = {
     data,
     columns: sliceColumns,
-    closeable,
     argColumns,
   };
 
@@ -79,6 +78,7 @@
       trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
       trackGroup: SCROLLING_TRACK_GROUP,
       params: trackConfig,
+      closeable,
     }),
   ];
   if (config?.pinned ?? true) {
@@ -117,7 +117,6 @@
 export interface CounterDebugTrackConfig {
   data: SqlDataSource;
   columns: CounterColumns;
-  closeable: boolean;
 }
 
 export interface CounterDebugTrackCreateConfig {
@@ -143,7 +142,6 @@
   const params: CounterDebugTrackConfig = {
     data,
     columns,
-    closeable,
   };
 
   const trackKey = uuidv4();
@@ -155,6 +153,7 @@
       trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
       trackGroup: SCROLLING_TRACK_GROUP,
       params,
+      closeable,
     }),
   ];
   if (config?.pinned ?? true) {
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 7bf69c9..fea7fb2 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -230,7 +230,9 @@
       case ProfileType.JAVA_HEAP_GRAPH:
         if (
           viewingOption ===
-          FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY
+            FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
+          viewingOption ===
+            FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
         ) {
           return RENDER_OBJ_COUNT;
         } else {
@@ -347,7 +349,9 @@
       current.viewingOption ===
         FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
       current.viewingOption ===
-        FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY
+        FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
+      current.viewingOption ===
+        FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
         ? 'B'
         : '';
     this.flamegraph.draw(ctx, width, height, 0, 0, unit);
@@ -355,7 +359,13 @@
 
   private onMouseClick({x, y}: {x: number; y: number}): boolean {
     const expandedCallsite = this.flamegraph.onMouseClick({x, y});
-    globals.dispatch(Actions.expandFlamegraphState({expandedCallsite}));
+    globals.state.currentFlamegraphState &&
+      globals.dispatch(
+        Actions.expandFlamegraphState({
+          expandedCallsite,
+          viewingOption: globals.state.currentFlamegraphState.viewingOption,
+        }),
+      );
     return true;
   }
 
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 30209ab..26dcb57 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -357,7 +357,7 @@
         label: 'Critical path',
         onclick: () =>
           runQuery(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
             this.engine,
           ).then(() =>
             addDebugSliceTrack(
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 2e15d67..4240ef4 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -37,6 +37,7 @@
 import {canvasClip} from '../common/canvas_utils';
 import {TimeScale} from './time_scale';
 import {getLegacySelection} from '../common/state';
+import {CloseTrackButton} from './close_track_button';
 
 function getTitleSize(title: string): string | undefined {
   const length = title.length;
@@ -330,6 +331,7 @@
   tags?: TrackTags;
   track?: Track;
   error?: Error | undefined;
+  closeable: boolean;
 
   // Issues a scrollTo() on this DOM element at creation time. Default: false.
   revealOnCreate?: boolean;
@@ -359,6 +361,7 @@
         m(TrackShell, {
           buttons: [
             attrs.error && m(CrashButton, {error: attrs.error}),
+            attrs.closeable && m(CloseTrackButton, {trackKey: attrs.trackKey}),
             attrs.buttons,
           ],
           title: attrs.title,
@@ -426,6 +429,7 @@
   tags?: TrackTags;
   trackFSM?: TrackCacheEntry;
   revealOnCreate?: boolean;
+  closeable: boolean;
 }
 
 export class TrackPanel implements Panel {
@@ -452,6 +456,7 @@
           trackKey: attrs.trackKey,
           error: attrs.trackFSM.getError(),
           track: attrs.trackFSM.track,
+          closeable: attrs.closeable,
         });
       }
       return m(TrackComponent, {
@@ -463,12 +468,14 @@
         track: attrs.trackFSM.track,
         error: attrs.trackFSM.getError(),
         revealOnCreate: attrs.revealOnCreate,
+        closeable: attrs.closeable,
       });
     } else {
       return m(TrackComponent, {
         trackKey: attrs.trackKey,
         title: attrs.title,
         revealOnCreate: attrs.revealOnCreate,
+        closeable: attrs.closeable,
       });
     }
   }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 3f12a7d..719705e 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -241,6 +241,7 @@
           title: trackBundle.title,
           tags: trackBundle.tags,
           trackFSM: trackBundle.trackFSM,
+          closeable: trackBundle.closeable,
         });
       },
     );
@@ -270,6 +271,7 @@
             title: trackBundle.title,
             tags: trackBundle.tags,
             trackFSM: trackBundle.trackFSM,
+            closeable: trackBundle.closeable,
           });
           childTracks.push(panel);
         }
@@ -327,6 +329,7 @@
               tags: trackBundle.tags,
               trackFSM: trackBundle.trackFSM,
               revealOnCreate: true,
+              closeable: trackBundle.closeable,
             });
           }),
           kind: 'TRACKS',
@@ -348,7 +351,7 @@
   // Resolve a track and its metadata through the track cache
   private resolveTrack(key: string): TrackBundle {
     const trackState = globals.state.tracks[key];
-    const {uri, params, name, labels} = trackState;
+    const {uri, params, name, labels, closeable} = trackState;
     const trackDesc = globals.trackManager.resolveTrackInfo(uri);
     const trackCacheEntry =
       trackDesc && globals.trackManager.resolveTrack(key, trackDesc, params);
@@ -361,6 +364,7 @@
       trackFSM,
       labels,
       trackIds,
+      closeable: closeable ?? false,
     };
   }
 
@@ -371,6 +375,7 @@
 
 interface TrackBundle {
   title: string;
+  closeable: boolean;
   trackFSM?: TrackCacheEntry;
   tags?: TrackTags;
   labels?: string[];
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS b/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS
new file mode 100644
index 0000000..52dc56a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/OWNERS
@@ -0,0 +1,2 @@
+lalitm@google.com
+ilkos@google.com
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
new file mode 100644
index 0000000..ebb10b8
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  LONG,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {
+  SimpleSliceTrack,
+  SimpleSliceTrackConfig,
+} from '../../frontend/simple_slice_track';
+
+class AndroidStartup implements Plugin {
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const e = ctx.engine;
+    await e.query(`include perfetto module android.startup.startups;`);
+
+    const cnt = await e.query('select count() cnt from android_startups');
+    if (cnt.firstRow({cnt: LONG}).cnt === 0n) {
+      return;
+    }
+
+    const config: SimpleSliceTrackConfig = {
+      data: {
+        sqlSource: `
+          SELECT l.ts AS ts, l.dur AS dur, l.package AS name
+          FROM android_startups l
+        `,
+        columns: ['ts', 'dur', 'name'],
+      },
+      columns: {ts: 'ts', dur: 'dur', name: 'name'},
+      argColumns: [],
+    };
+    ctx.registerStaticTrack({
+      uri: `dev.perfetto.AndroidStartup#startups`,
+      displayName: 'Android App Startups',
+      trackFactory: (trackCtx) => {
+        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'dev.perfetto.AndroidStartup',
+  plugin: AndroidStartup,
+};
diff --git a/ui/src/plugins/dev.perfetto.Chaos/OWNERS b/ui/src/plugins/dev.perfetto.Chaos/OWNERS
new file mode 100644
index 0000000..9ee9fce
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Chaos/OWNERS
@@ -0,0 +1 @@
+hjd@google.com
diff --git a/ui/src/plugins/dev.perfetto.Chaos/index.ts b/ui/src/plugins/dev.perfetto.Chaos/index.ts
new file mode 100644
index 0000000..c18ff7f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.Chaos/index.ts
@@ -0,0 +1,81 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+  addDebugSliceTrack,
+} from '../../public';
+
+class Chaos implements Plugin {
+  onActivate(ctx: PluginContext): void {
+    ctx.registerCommand({
+      id: 'dev.perfetto.Chaos#CrashNow',
+      name: 'Chaos: crash now',
+      callback: () => {
+        throw new Error('Manual crash from dev.perfetto.Chaos#CrashNow');
+      },
+    });
+  }
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    ctx.registerCommand({
+      id: 'dev.perfetto.Chaos#CrashNowQuery',
+      name: 'Chaos: run crashing query',
+      callback: () => {
+        ctx.engine.query(`this is a
+          syntactically
+          invalid
+          query
+          over
+          many
+          lines
+        `);
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'dev.perfetto.Chaos#AddCrashingDebugTrack',
+      name: 'Chaos: add crashing debug track',
+      callback: () => {
+        addDebugSliceTrack(
+          ctx.engine,
+          {
+            sqlSource: `
+            syntactically
+            invalid
+            query
+            over
+            many
+          `,
+          },
+          `Chaos track`,
+          {ts: 'ts', dur: 'dur', name: 'name'},
+          [],
+        );
+      },
+    });
+  }
+
+  async onTraceUnload(_: PluginContextTrace): Promise<void> {}
+
+  onDeactivate(_: PluginContext): void {}
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'dev.perfetto.Chaos',
+  plugin: Chaos,
+};
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 19c1867..97cee3c 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -21,7 +21,7 @@
 import {LegacySelection} from '../common/state';
 import {PanelSize} from '../frontend/panel';
 import {EngineProxy} from '../trace_processor/engine';
-import {UntypedEventSet} from '../common/event_set';
+import {UntypedEventSet} from '../core/event_set';
 
 export {EngineProxy} from '../trace_processor/engine';
 export {
diff --git a/ui/src/tracks/debug/counter_track.ts b/ui/src/tracks/debug/counter_track.ts
index d61d573..5b37e3d 100644
--- a/ui/src/tracks/debug/counter_track.ts
+++ b/ui/src/tracks/debug/counter_track.ts
@@ -14,10 +14,7 @@
 
 import m from 'mithril';
 
-import {Actions} from '../../common/actions';
 import {BaseCounterTrack} from '../../frontend/base_counter_track';
-import {globals} from '../../frontend/globals';
-import {TrackButton} from '../../frontend/track_panel';
 import {TrackContext} from '../../public';
 import {EngineProxy} from '../../trace_processor/engine';
 import {CounterDebugTrackConfig} from '../../frontend/debug_tracks';
@@ -49,20 +46,7 @@
   }
 
   getTrackShellButtons(): m.Children {
-    return [
-      this.getCounterContextMenu(),
-      this.config.closeable &&
-        m(TrackButton, {
-          action: () => {
-            globals.dispatch(
-              Actions.removeTracks({trackKeys: [this.trackKey]}),
-            );
-          },
-          i: 'close',
-          tooltip: 'Close',
-          showButton: true,
-        }),
-    ];
+    return this.getCounterContextMenu();
   }
 
   getSqlSource(): string {
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index dbab2be..4ea16dc 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -12,12 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
-import {TrackButton} from '../../frontend/track_panel';
 import {TrackContext} from '../../public';
 import {EngineProxy} from '../../trace_processor/engine';
 import {
@@ -38,7 +33,6 @@
 export interface DebugTrackV2Config {
   data: SqlDataSource;
   columns: SliceColumns;
-  closeable: boolean;
   argColumns: string[];
 }
 
@@ -81,21 +75,6 @@
     };
   }
 
-  getTrackShellButtons(): m.Children {
-    return this.config.closeable
-      ? m(TrackButton, {
-          action: () => {
-            globals.dispatch(
-              Actions.removeTracks({trackKeys: [this.trackKey]}),
-            );
-          },
-          i: 'close',
-          tooltip: 'Close',
-          showButton: true,
-        })
-      : [];
-  }
-
   private async createTrackTable(
     data: SqlDataSource,
     sliceColumns: SliceColumns,
diff --git a/ui/src/tracks/ftrace/ftrace_explorer.ts b/ui/src/tracks/ftrace/ftrace_explorer.ts
index 7015d5c..99a2ce2 100644
--- a/ui/src/tracks/ftrace/ftrace_explorer.ts
+++ b/ui/src/tracks/ftrace/ftrace_explorer.ts
@@ -39,14 +39,15 @@
   STR_NULL,
 } from '../../public';
 import {raf} from '../../core/raf_scheduler';
-import {VisibleState} from '../../common/state';
+import {AsyncLimiter} from '../../base/async_limiter';
+import {Monitor} from '../../base/monitor';
 
 const ROW_H = 20;
 const PAGE_SIZE = 250;
 
 interface FtraceExplorerAttrs {
   counters: FtraceStat[];
-  store: Store<FtraceFilter>;
+  filterStore: Store<FtraceFilter>;
   engine: EngineProxy;
 }
 
@@ -72,32 +73,37 @@
 }
 
 export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
-  private paginationStore = createStore<Pagination>({
+  private readonly paginationStore = createStore<Pagination>({
     page: 0,
     pageCount: 0,
   });
-  private oldPagination?: Pagination;
-  private oldFilterState?: FtraceFilter;
-  private oldVisibleState?: VisibleState;
+  private readonly monitor: Monitor;
+  private readonly queryLimiter = new AsyncLimiter();
 
   // A cache of the data we have most recently loaded from our store
   private data?: FtracePanelData;
 
+  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
+    this.monitor = new Monitor([
+      () => globals.state.frontendLocalState.visibleState.start,
+      () => globals.state.frontendLocalState.visibleState.end,
+      () => attrs.filterStore.state,
+      () => this.paginationStore.state,
+    ]);
+  }
+
   view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
-    if (this.shouldUpdate(attrs.store)) {
-      this.oldVisibleState = globals.state.frontendLocalState.visibleState;
-      this.oldFilterState = attrs.store.state;
-      this.oldPagination = this.paginationStore.state;
-      lookupFtraceEvents(
-        attrs.engine,
-        this.paginationStore.state.page * PAGE_SIZE,
-        this.paginationStore.state.pageCount * PAGE_SIZE,
-        attrs.store.state,
-      ).then((data) => {
-        this.data = data;
+    this.monitor.ifStateChanged(() =>
+      this.queryLimiter.schedule(async () => {
+        this.data = await lookupFtraceEvents(
+          attrs.engine,
+          this.paginationStore.state.page * PAGE_SIZE,
+          this.paginationStore.state.pageCount * PAGE_SIZE,
+          attrs.filterStore.state,
+        );
         raf.scheduleFullRedraw();
-      });
-    }
+      }),
+    );
 
     return m(
       DetailsShell,
@@ -124,7 +130,7 @@
     const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
 
     // Work out which "page" we're on
-    const page = Math.floor(visibleRowOffset / PAGE_SIZE) - 1;
+    const page = Math.max(0, Math.floor(visibleRowOffset / PAGE_SIZE) - 1);
     const pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2;
 
     if (page !== prevPage || pageCount !== prevPageCount) {
@@ -136,24 +142,6 @@
     }
   }
 
-  private shouldUpdate(filterStore: Store<FtraceFilter>): boolean {
-    if (filterStore.state !== this.oldFilterState) {
-      return true;
-    }
-
-    if (
-      globals.state.frontendLocalState.visibleState !== this.oldVisibleState
-    ) {
-      return true;
-    }
-
-    if (this.paginationStore.state !== this.oldPagination) {
-      return true;
-    }
-
-    return false;
-  }
-
   onRowOver(ts: time) {
     globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
   }
@@ -172,7 +160,7 @@
   }
 
   private renderFilterPanel(attrs: FtraceExplorerAttrs) {
-    const excludeList = attrs.store.state.excludeList;
+    const excludeList = attrs.filterStore.state.excludeList;
     const options: MultiSelectOption[] = attrs.counters.map(({name, count}) => {
       return {
         id: name,
@@ -196,7 +184,7 @@
             newList.add(id);
           }
         });
-        attrs.store.edit((draft) => {
+        attrs.filterStore.edit((draft) => {
           draft.excludeList = Array.from(newList);
         });
       },
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index c5860b9..a58c27a 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -85,7 +85,11 @@
       isEphemeral: false,
       content: {
         render: () =>
-          m(FtraceExplorer, {counters, store: filterStore, engine: ctx.engine}),
+          m(FtraceExplorer, {
+            counters,
+            filterStore,
+            engine: ctx.engine,
+          }),
         getTitle: () => 'Ftrace Events',
       },
     });