Merge "ui: Remove NullTrack plugin from default list as it no longer exists" into main
diff --git a/Android.bp b/Android.bp
index 43d2027..94b84ff 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1320,6 +1320,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -2344,6 +2345,7 @@
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
         ":perfetto_src_trace_processor_importers_etw_full",
+        ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":perfetto_src_trace_processor_importers_ftrace_full",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
@@ -2889,6 +2891,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -2916,6 +2919,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.gen.cc",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/protolog_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.gen.cc",
@@ -2943,6 +2947,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.gen.h",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.gen.h",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.gen.h",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.gen.h",
         "external/perfetto/protos/perfetto/config/android/protolog_config.gen.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.gen.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.gen.h",
@@ -2965,6 +2970,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -2991,6 +2997,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.pb.cc",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/protolog_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pb.cc",
@@ -3017,6 +3024,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.pb.h",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.pb.h",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.pb.h",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.pb.h",
         "external/perfetto/protos/perfetto/config/android/protolog_config.pb.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pb.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pb.h",
@@ -3039,6 +3047,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -3066,6 +3075,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.pbzero.cc",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/protolog_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pbzero.cc",
@@ -3093,6 +3103,7 @@
         "external/perfetto/protos/perfetto/config/android/android_system_property_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/android/network_trace_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/android/packages_list_config.pbzero.h",
+        "external/perfetto/protos/perfetto/config/android/pixel_modem_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/android/protolog_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pbzero.h",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pbzero.h",
@@ -3226,6 +3237,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -5156,6 +5168,7 @@
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
+        "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
@@ -5245,6 +5258,7 @@
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
+        "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
@@ -5309,6 +5323,7 @@
 genrule {
     name: "perfetto_protos_perfetto_metrics_webview_descriptor",
     srcs: [
+        ":libprotobuf-internal-descriptor-proto",
         "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",
@@ -5317,6 +5332,7 @@
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
+        "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
@@ -5373,7 +5389,7 @@
     tools: [
         "aprotoc",
     ],
-    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)",
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --proto_path=external/protobuf/src --descriptor_set_out=$(out) $(in)",
     out: [
         "perfetto_protos_perfetto_metrics_webview_descriptor.bin",
     ],
@@ -5394,6 +5410,7 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -5426,6 +5443,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/network_trace.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/packages_list.gen.cc",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/protolog.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.gen.cc",
@@ -5458,6 +5476,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.gen.h",
         "external/perfetto/protos/perfetto/trace/android/network_trace.gen.h",
         "external/perfetto/protos/perfetto/trace/android/packages_list.gen.h",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.gen.h",
         "external/perfetto/protos/perfetto/trace/android/protolog.gen.h",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.gen.h",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.gen.h",
@@ -5485,6 +5504,7 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -5516,6 +5536,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/network_trace.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/packages_list.pb.cc",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/protolog.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.pb.cc",
@@ -5547,6 +5568,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.pb.h",
         "external/perfetto/protos/perfetto/trace/android/network_trace.pb.h",
         "external/perfetto/protos/perfetto/trace/android/packages_list.pb.h",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.pb.h",
         "external/perfetto/protos/perfetto/trace/android/protolog.pb.h",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.pb.h",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.pb.h",
@@ -5611,6 +5633,7 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -5643,6 +5666,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/network_trace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/packages_list.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/protolog.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.pbzero.cc",
@@ -5675,6 +5699,7 @@
         "external/perfetto/protos/perfetto/trace/android/initial_display_state.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/network_trace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/packages_list.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/android/pixel_modem_events.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/protolog.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/shell_transition.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/surfaceflinger_common.pbzero.h",
@@ -5882,6 +5907,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -5920,6 +5946,7 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -11281,6 +11308,7 @@
     name: "perfetto_src_trace_processor_containers_unittests",
     srcs: [
         "src/trace_processor/containers/bit_vector_unittest.cc",
+        "src/trace_processor/containers/implicit_segment_forest_unittest.cc",
         "src/trace_processor/containers/null_term_string_view_unittest.cc",
         "src/trace_processor/containers/row_map_unittest.cc",
         "src/trace_processor/containers/string_pool_unittest.cc",
@@ -11320,6 +11348,7 @@
     srcs: [
         "src/trace_processor/db/column/arrangement_overlay_unittest.cc",
         "src/trace_processor/db/column/dense_null_overlay_unittest.cc",
+        "src/trace_processor/db/column/fake_storage_unittest.cc",
         "src/trace_processor/db/column/id_storage_unittest.cc",
         "src/trace_processor/db/column/null_overlay_unittest.cc",
         "src/trace_processor/db/column/numeric_storage_unittest.cc",
@@ -11484,11 +11513,20 @@
 filegroup {
     name: "perfetto_src_trace_processor_importers_etw_full",
     srcs: [
-        "src/trace_processor/importers/etw/etw_module.cc",
+        "src/trace_processor/importers/etw/etw_module_impl.cc",
+        "src/trace_processor/importers/etw/etw_parser.cc",
         "src/trace_processor/importers/etw/etw_tokenizer.cc",
     ],
 }
 
+// GN: //src/trace_processor/importers/etw:minimal
+filegroup {
+    name: "perfetto_src_trace_processor_importers_etw_minimal",
+    srcs: [
+        "src/trace_processor/importers/etw/etw_module.cc",
+    ],
+}
+
 // GN: //src/trace_processor/importers/ftrace:ftrace_descriptors
 filegroup {
     name: "perfetto_src_trace_processor_importers_ftrace_ftrace_descriptors",
@@ -12005,6 +12043,7 @@
         "src/trace_processor/metrics/sql/android/android_multiuser.sql",
         "src/trace_processor/metrics/sql/android/android_multiuser_populator.sql",
         "src/trace_processor/metrics/sql/android/android_netperf.sql",
+        "src/trace_processor/metrics/sql/android/android_oom_adjuster.sql",
         "src/trace_processor/metrics/sql/android/android_other_traces.sql",
         "src/trace_processor/metrics/sql/android/android_package_list.sql",
         "src/trace_processor/metrics/sql/android/android_powrails.sql",
@@ -12253,6 +12292,8 @@
 filegroup {
     name: "perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
     srcs: [
+        "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc",
     ],
@@ -12380,6 +12421,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/dvfs.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/frames/timeline_maxsdk28.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/freezer.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/input.sql",
@@ -12398,6 +12440,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk29.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/startup/time_to_display.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/statsd.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/suspend.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/thread.sql",
@@ -12452,6 +12495,10 @@
         "src/trace_processor/perfetto_sql/stdlib/stack_trace/jit.sql",
         "src/trace_processor/perfetto_sql/stdlib/time/conversion.sql",
         "src/trace_processor/perfetto_sql/stdlib/v8/jit.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/arm_dsu.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql",
@@ -12858,7 +12905,8 @@
 filegroup {
     name: "perfetto_src_trace_redaction_trace_redaction",
     srcs: [
-        "src/trace_redaction/build_timeline.cc",
+        "src/trace_redaction/collect_frame_cookies.cc",
+        "src/trace_redaction/collect_timeline_events.cc",
         "src/trace_redaction/filter_ftrace_using_allowlist.cc",
         "src/trace_redaction/filter_packet_using_allowlist.cc",
         "src/trace_redaction/filter_print_events.cc",
@@ -12878,6 +12926,7 @@
         "src/trace_redaction/scrub_process_stats.cc",
         "src/trace_redaction/scrub_process_trees.cc",
         "src/trace_redaction/scrub_trace_packet.cc",
+        "src/trace_redaction/suspend_resume.cc",
         "src/trace_redaction/trace_redaction_framework.cc",
         "src/trace_redaction/trace_redactor.cc",
     ],
@@ -12887,7 +12936,8 @@
 filegroup {
     name: "perfetto_src_trace_redaction_unittests",
     srcs: [
-        "src/trace_redaction/build_timeline_unittest.cc",
+        "src/trace_redaction/collect_frame_cookies_unittest.cc",
+        "src/trace_redaction/collect_timeline_events_unittest.cc",
         "src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc",
         "src/trace_redaction/filter_packet_using_allowlist_unittest.cc",
         "src/trace_redaction/filter_sched_waking_events_unittest.cc",
@@ -12899,6 +12949,7 @@
         "src/trace_redaction/redact_process_free_unittest.cc",
         "src/trace_redaction/redact_sched_switch_unittest.cc",
         "src/trace_redaction/redact_task_newtask_unittest.cc",
+        "src/trace_redaction/suspend_resume_unittest.cc",
     ],
 }
 
@@ -13745,8 +13796,8 @@
 }
 
 // GN: [//protos/perfetto/trace:non_minimal_source_set, //protos/perfetto/trace:minimal_source_set]
-java_library {
-    name: "perfetto_trace_java_protos",
+filegroup {
+    name: "perfetto_trace_filegroup_proto",
     srcs: [
         "protos/perfetto/common/android_energy_consumer_descriptor.proto",
         "protos/perfetto/common/android_log_constants.proto",
@@ -13773,6 +13824,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -13811,6 +13863,225 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
+        "protos/perfetto/trace/android/protolog.proto",
+        "protos/perfetto/trace/android/shell_transition.proto",
+        "protos/perfetto/trace/android/surfaceflinger_common.proto",
+        "protos/perfetto/trace/android/surfaceflinger_layers.proto",
+        "protos/perfetto/trace/android/surfaceflinger_transactions.proto",
+        "protos/perfetto/trace/chrome/chrome_benchmark_metadata.proto",
+        "protos/perfetto/trace/chrome/chrome_metadata.proto",
+        "protos/perfetto/trace/chrome/chrome_trace_event.proto",
+        "protos/perfetto/trace/chrome/chrome_trigger.proto",
+        "protos/perfetto/trace/chrome/v8.proto",
+        "protos/perfetto/trace/clock_snapshot.proto",
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+        "protos/perfetto/trace/extension_descriptor.proto",
+        "protos/perfetto/trace/filesystem/inode_file_map.proto",
+        "protos/perfetto/trace/ftrace/android_fs.proto",
+        "protos/perfetto/trace/ftrace/binder.proto",
+        "protos/perfetto/trace/ftrace/block.proto",
+        "protos/perfetto/trace/ftrace/cgroup.proto",
+        "protos/perfetto/trace/ftrace/clk.proto",
+        "protos/perfetto/trace/ftrace/cma.proto",
+        "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpuhp.proto",
+        "protos/perfetto/trace/ftrace/cros_ec.proto",
+        "protos/perfetto/trace/ftrace/dma_fence.proto",
+        "protos/perfetto/trace/ftrace/dmabuf_heap.proto",
+        "protos/perfetto/trace/ftrace/dpu.proto",
+        "protos/perfetto/trace/ftrace/drm.proto",
+        "protos/perfetto/trace/ftrace/ext4.proto",
+        "protos/perfetto/trace/ftrace/f2fs.proto",
+        "protos/perfetto/trace/ftrace/fastrpc.proto",
+        "protos/perfetto/trace/ftrace/fence.proto",
+        "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/ftrace.proto",
+        "protos/perfetto/trace/ftrace/ftrace_event.proto",
+        "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
+        "protos/perfetto/trace/ftrace/ftrace_stats.proto",
+        "protos/perfetto/trace/ftrace/g2d.proto",
+        "protos/perfetto/trace/ftrace/generic.proto",
+        "protos/perfetto/trace/ftrace/gpu_mem.proto",
+        "protos/perfetto/trace/ftrace/gpu_scheduler.proto",
+        "protos/perfetto/trace/ftrace/hyp.proto",
+        "protos/perfetto/trace/ftrace/i2c.proto",
+        "protos/perfetto/trace/ftrace/ion.proto",
+        "protos/perfetto/trace/ftrace/ipi.proto",
+        "protos/perfetto/trace/ftrace/irq.proto",
+        "protos/perfetto/trace/ftrace/kmem.proto",
+        "protos/perfetto/trace/ftrace/kvm.proto",
+        "protos/perfetto/trace/ftrace/lowmemorykiller.proto",
+        "protos/perfetto/trace/ftrace/lwis.proto",
+        "protos/perfetto/trace/ftrace/mali.proto",
+        "protos/perfetto/trace/ftrace/mdss.proto",
+        "protos/perfetto/trace/ftrace/mm_event.proto",
+        "protos/perfetto/trace/ftrace/net.proto",
+        "protos/perfetto/trace/ftrace/oom.proto",
+        "protos/perfetto/trace/ftrace/panel.proto",
+        "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
+        "protos/perfetto/trace/ftrace/raw_syscalls.proto",
+        "protos/perfetto/trace/ftrace/regulator.proto",
+        "protos/perfetto/trace/ftrace/rpm.proto",
+        "protos/perfetto/trace/ftrace/samsung.proto",
+        "protos/perfetto/trace/ftrace/sched.proto",
+        "protos/perfetto/trace/ftrace/scm.proto",
+        "protos/perfetto/trace/ftrace/sde.proto",
+        "protos/perfetto/trace/ftrace/signal.proto",
+        "protos/perfetto/trace/ftrace/skb.proto",
+        "protos/perfetto/trace/ftrace/sock.proto",
+        "protos/perfetto/trace/ftrace/sync.proto",
+        "protos/perfetto/trace/ftrace/synthetic.proto",
+        "protos/perfetto/trace/ftrace/systrace.proto",
+        "protos/perfetto/trace/ftrace/task.proto",
+        "protos/perfetto/trace/ftrace/tcp.proto",
+        "protos/perfetto/trace/ftrace/test_bundle_wrapper.proto",
+        "protos/perfetto/trace/ftrace/thermal.proto",
+        "protos/perfetto/trace/ftrace/trusty.proto",
+        "protos/perfetto/trace/ftrace/ufs.proto",
+        "protos/perfetto/trace/ftrace/v4l2.proto",
+        "protos/perfetto/trace/ftrace/virtio_gpu.proto",
+        "protos/perfetto/trace/ftrace/virtio_video.proto",
+        "protos/perfetto/trace/ftrace/vmscan.proto",
+        "protos/perfetto/trace/ftrace/workqueue.proto",
+        "protos/perfetto/trace/gpu/gpu_counter_event.proto",
+        "protos/perfetto/trace/gpu/gpu_log.proto",
+        "protos/perfetto/trace/gpu/gpu_render_stage_event.proto",
+        "protos/perfetto/trace/gpu/vulkan_api_event.proto",
+        "protos/perfetto/trace/gpu/vulkan_memory_event.proto",
+        "protos/perfetto/trace/interned_data/interned_data.proto",
+        "protos/perfetto/trace/memory_graph.proto",
+        "protos/perfetto/trace/perfetto/perfetto_metatrace.proto",
+        "protos/perfetto/trace/perfetto/tracing_service_event.proto",
+        "protos/perfetto/trace/power/android_energy_estimation_breakdown.proto",
+        "protos/perfetto/trace/power/android_entity_state_residency.proto",
+        "protos/perfetto/trace/power/battery_counters.proto",
+        "protos/perfetto/trace/power/power_rails.proto",
+        "protos/perfetto/trace/profiling/deobfuscation.proto",
+        "protos/perfetto/trace/profiling/heap_graph.proto",
+        "protos/perfetto/trace/profiling/profile_common.proto",
+        "protos/perfetto/trace/profiling/profile_packet.proto",
+        "protos/perfetto/trace/profiling/smaps.proto",
+        "protos/perfetto/trace/ps/process_stats.proto",
+        "protos/perfetto/trace/ps/process_tree.proto",
+        "protos/perfetto/trace/remote_clock_sync.proto",
+        "protos/perfetto/trace/statsd/statsd_atom.proto",
+        "protos/perfetto/trace/sys_stats/sys_stats.proto",
+        "protos/perfetto/trace/system_info.proto",
+        "protos/perfetto/trace/system_info/cpu_info.proto",
+        "protos/perfetto/trace/test_event.proto",
+        "protos/perfetto/trace/test_extensions.proto",
+        "protos/perfetto/trace/trace.proto",
+        "protos/perfetto/trace/trace_packet.proto",
+        "protos/perfetto/trace/trace_packet_defaults.proto",
+        "protos/perfetto/trace/trace_uuid.proto",
+        "protos/perfetto/trace/track_event/chrome_active_processes.proto",
+        "protos/perfetto/trace/track_event/chrome_application_state_info.proto",
+        "protos/perfetto/trace/track_event/chrome_compositor_scheduler_state.proto",
+        "protos/perfetto/trace/track_event/chrome_content_settings_event_info.proto",
+        "protos/perfetto/trace/track_event/chrome_frame_reporter.proto",
+        "protos/perfetto/trace/track_event/chrome_histogram_sample.proto",
+        "protos/perfetto/trace/track_event/chrome_keyed_service.proto",
+        "protos/perfetto/trace/track_event/chrome_latency_info.proto",
+        "protos/perfetto/trace/track_event/chrome_legacy_ipc.proto",
+        "protos/perfetto/trace/track_event/chrome_message_pump.proto",
+        "protos/perfetto/trace/track_event/chrome_mojo_event_info.proto",
+        "protos/perfetto/trace/track_event/chrome_process_descriptor.proto",
+        "protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto",
+        "protos/perfetto/trace/track_event/chrome_thread_descriptor.proto",
+        "protos/perfetto/trace/track_event/chrome_user_event.proto",
+        "protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto",
+        "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",
+        "protos/perfetto/trace/track_event/source_location.proto",
+        "protos/perfetto/trace/track_event/task_execution.proto",
+        "protos/perfetto/trace/track_event/thread_descriptor.proto",
+        "protos/perfetto/trace/track_event/track_descriptor.proto",
+        "protos/perfetto/trace/track_event/track_event.proto",
+        "protos/perfetto/trace/translation/translation_table.proto",
+        "protos/perfetto/trace/trigger.proto",
+        "protos/perfetto/trace/ui_state.proto",
+    ],
+}
+
+// GN: [//protos/perfetto/trace:non_minimal_source_set, //protos/perfetto/trace:minimal_source_set]
+java_library {
+    name: "perfetto_trace_java_protos",
+    srcs: [
+        "protos/perfetto/common/android_energy_consumer_descriptor.proto",
+        "protos/perfetto/common/android_log_constants.proto",
+        "protos/perfetto/common/builtin_clock.proto",
+        "protos/perfetto/common/commit_data_request.proto",
+        "protos/perfetto/common/data_source_descriptor.proto",
+        "protos/perfetto/common/descriptor.proto",
+        "protos/perfetto/common/ftrace_descriptor.proto",
+        "protos/perfetto/common/gpu_counter_descriptor.proto",
+        "protos/perfetto/common/interceptor_descriptor.proto",
+        "protos/perfetto/common/observable_events.proto",
+        "protos/perfetto/common/perf_events.proto",
+        "protos/perfetto/common/protolog_common.proto",
+        "protos/perfetto/common/sys_stats_counters.proto",
+        "protos/perfetto/common/trace_stats.proto",
+        "protos/perfetto/common/tracing_service_capabilities.proto",
+        "protos/perfetto/common/tracing_service_state.proto",
+        "protos/perfetto/common/track_event_descriptor.proto",
+        "protos/perfetto/config/android/android_game_intervention_list_config.proto",
+        "protos/perfetto/config/android/android_input_event_config.proto",
+        "protos/perfetto/config/android/android_log_config.proto",
+        "protos/perfetto/config/android/android_polled_state_config.proto",
+        "protos/perfetto/config/android/android_sdk_sysprop_guard_config.proto",
+        "protos/perfetto/config/android/android_system_property_config.proto",
+        "protos/perfetto/config/android/network_trace_config.proto",
+        "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
+        "protos/perfetto/config/android/protolog_config.proto",
+        "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
+        "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
+        "protos/perfetto/config/chrome/chrome_config.proto",
+        "protos/perfetto/config/chrome/scenario_config.proto",
+        "protos/perfetto/config/chrome/v8_config.proto",
+        "protos/perfetto/config/data_source_config.proto",
+        "protos/perfetto/config/etw/etw_config.proto",
+        "protos/perfetto/config/ftrace/ftrace_config.proto",
+        "protos/perfetto/config/gpu/gpu_counter_config.proto",
+        "protos/perfetto/config/gpu/vulkan_memory_config.proto",
+        "protos/perfetto/config/inode_file/inode_file_config.proto",
+        "protos/perfetto/config/interceptor_config.proto",
+        "protos/perfetto/config/interceptors/console_config.proto",
+        "protos/perfetto/config/power/android_power_config.proto",
+        "protos/perfetto/config/process_stats/process_stats_config.proto",
+        "protos/perfetto/config/profiling/heapprofd_config.proto",
+        "protos/perfetto/config/profiling/java_hprof_config.proto",
+        "protos/perfetto/config/profiling/perf_event_config.proto",
+        "protos/perfetto/config/statsd/atom_ids.proto",
+        "protos/perfetto/config/statsd/statsd_tracing_config.proto",
+        "protos/perfetto/config/stress_test_config.proto",
+        "protos/perfetto/config/sys_stats/sys_stats_config.proto",
+        "protos/perfetto/config/system_info/system_info.proto",
+        "protos/perfetto/config/test_config.proto",
+        "protos/perfetto/config/trace_config.proto",
+        "protos/perfetto/config/track_event/track_event_config.proto",
+        "protos/perfetto/trace/android/android_game_intervention_list.proto",
+        "protos/perfetto/trace/android/android_input_event.proto",
+        "protos/perfetto/trace/android/android_log.proto",
+        "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/camera_event.proto",
+        "protos/perfetto/trace/android/frame_timeline_event.proto",
+        "protos/perfetto/trace/android/gpu_mem_event.proto",
+        "protos/perfetto/trace/android/graphics_frame_event.proto",
+        "protos/perfetto/trace/android/initial_display_state.proto",
+        "protos/perfetto/trace/android/network_trace.proto",
+        "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -14321,6 +14592,7 @@
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
         ":perfetto_src_trace_processor_importers_common_unittests",
         ":perfetto_src_trace_processor_importers_etw_full",
+        ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":perfetto_src_trace_processor_importers_ftrace_full",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
@@ -15048,6 +15320,7 @@
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
         ":perfetto_src_trace_processor_importers_etw_full",
+        ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":perfetto_src_trace_processor_importers_ftrace_full",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
@@ -15268,6 +15541,7 @@
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
+        ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
         ":perfetto_src_trace_processor_importers_json_minimal",
@@ -15420,6 +15694,7 @@
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
         ":perfetto_src_trace_processor_importers_etw_full",
+        ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":perfetto_src_trace_processor_importers_ftrace_full",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
@@ -16106,11 +16381,10 @@
     ],
 }
 
-// TODO(b/315118713): use list of proto file sources instead of merged proto
 gensrcs {
     name: "perfetto_trace_javastream_protos",
     srcs: [
-        "protos/perfetto/trace/perfetto_trace.proto",
+        ":perfetto_trace_filegroup_proto",
     ],
     tools: [
         "aprotoc",
diff --git a/Android.bp.extras b/Android.bp.extras
index 3aab5af..3ebf51e 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -172,11 +172,10 @@
     ],
 }
 
-// TODO(b/315118713): use list of proto file sources instead of merged proto
 gensrcs {
     name: "perfetto_trace_javastream_protos",
     srcs: [
-        "protos/perfetto/trace/perfetto_trace.proto",
+        ":perfetto_trace_filegroup_proto",
     ],
     tools: [
         "aprotoc",
diff --git a/BUILD b/BUILD
index 2b53c16..c6408f1 100644
--- a/BUILD
+++ b/BUILD
@@ -225,6 +225,7 @@
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
         ":src_trace_processor_importers_etw_full",
+        ":src_trace_processor_importers_etw_minimal",
         ":src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":src_trace_processor_importers_ftrace_full",
         ":src_trace_processor_importers_ftrace_minimal",
@@ -1365,6 +1366,7 @@
         ":include_perfetto_public_base",
         ":include_perfetto_public_protozero",
         "src/trace_processor/containers/bit_vector.h",
+        "src/trace_processor/containers/implicit_segment_forest.h",
         "src/trace_processor/containers/null_term_string_view.h",
         "src/trace_processor/containers/row_map.h",
         "src/trace_processor/containers/row_map_algorithms.h",
@@ -1526,13 +1528,24 @@
 perfetto_filegroup(
     name = "src_trace_processor_importers_etw_full",
     srcs = [
-        "src/trace_processor/importers/etw/etw_module.cc",
-        "src/trace_processor/importers/etw/etw_module.h",
+        "src/trace_processor/importers/etw/etw_module_impl.cc",
+        "src/trace_processor/importers/etw/etw_module_impl.h",
+        "src/trace_processor/importers/etw/etw_parser.cc",
+        "src/trace_processor/importers/etw/etw_parser.h",
         "src/trace_processor/importers/etw/etw_tokenizer.cc",
         "src/trace_processor/importers/etw/etw_tokenizer.h",
     ],
 )
 
+# GN target: //src/trace_processor/importers/etw:minimal
+perfetto_filegroup(
+    name = "src_trace_processor_importers_etw_minimal",
+    srcs = [
+        "src/trace_processor/importers/etw/etw_module.cc",
+        "src/trace_processor/importers/etw/etw_module.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/ftrace:ftrace_descriptors
 perfetto_filegroup(
     name = "src_trace_processor_importers_ftrace_ftrace_descriptors",
@@ -1985,6 +1998,7 @@
         "src/trace_processor/metrics/sql/android/android_multiuser.sql",
         "src/trace_processor/metrics/sql/android/android_multiuser_populator.sql",
         "src/trace_processor/metrics/sql/android/android_netperf.sql",
+        "src/trace_processor/metrics/sql/android/android_oom_adjuster.sql",
         "src/trace_processor/metrics/sql/android/android_other_traces.sql",
         "src/trace_processor/metrics/sql/android/android_package_list.sql",
         "src/trace_processor/metrics/sql/android/android_powrails.sql",
@@ -2300,6 +2314,10 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_intrinsics_operators_operators",
     srcs = [
+        "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h",
         "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
         "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h",
         "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc",
@@ -2392,6 +2410,7 @@
     srcs = [
         "src/trace_processor/perfetto_sql/stdlib/android/frames/per_frame_metrics.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/frames/timeline_maxsdk28.sql",
     ],
 )
 
@@ -2404,6 +2423,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk29.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/startups_minsdk33.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/startup/time_to_display.sql",
     ],
 )
 
@@ -2607,6 +2627,17 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/viz/summary:summary
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_viz_summary_summary",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/wattson:wattson
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_wattson_wattson",
@@ -2642,6 +2673,7 @@
         ":src_trace_processor_perfetto_sql_stdlib_stack_trace_stack_trace",
         ":src_trace_processor_perfetto_sql_stdlib_time_time",
         ":src_trace_processor_perfetto_sql_stdlib_v8_v8",
+        ":src_trace_processor_perfetto_sql_stdlib_viz_summary_summary",
         ":src_trace_processor_perfetto_sql_stdlib_wattson_wattson",
     ],
     outs = [
@@ -3880,6 +3912,7 @@
         "protos/perfetto/config/android/android_system_property_config.proto",
         "protos/perfetto/config/android/network_trace_config.proto",
         "protos/perfetto/config/android/packages_list_config.proto",
+        "protos/perfetto/config/android/pixel_modem_config.proto",
         "protos/perfetto/config/android/protolog_config.proto",
         "protos/perfetto/config/android/surfaceflinger_layers_config.proto",
         "protos/perfetto/config/android/surfaceflinger_transactions_config.proto",
@@ -4414,6 +4447,7 @@
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto",
+        "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/anr_metric.proto",
@@ -4591,6 +4625,7 @@
         "protos/perfetto/trace/android/initial_display_state.proto",
         "protos/perfetto/trace/android/network_trace.proto",
         "protos/perfetto/trace/android/packages_list.proto",
+        "protos/perfetto/trace/android/pixel_modem_events.proto",
         "protos/perfetto/trace/android/protolog.proto",
         "protos/perfetto/trace/android/shell_transition.proto",
         "protos/perfetto/trace/android/surfaceflinger_common.proto",
@@ -5707,6 +5742,7 @@
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
         ":src_trace_processor_importers_etw_full",
+        ":src_trace_processor_importers_etw_minimal",
         ":src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":src_trace_processor_importers_ftrace_full",
         ":src_trace_processor_importers_ftrace_minimal",
@@ -5876,6 +5912,7 @@
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
         ":src_trace_processor_importers_etw_full",
+        ":src_trace_processor_importers_etw_minimal",
         ":src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":src_trace_processor_importers_ftrace_full",
         ":src_trace_processor_importers_ftrace_minimal",
@@ -6103,6 +6140,7 @@
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
         ":src_trace_processor_importers_etw_full",
+        ":src_trace_processor_importers_etw_minimal",
         ":src_trace_processor_importers_ftrace_ftrace_descriptors",
         ":src_trace_processor_importers_ftrace_full",
         ":src_trace_processor_importers_ftrace_minimal",
diff --git a/CHANGELOG b/CHANGELOG
index 788d61f..99ff035 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,9 +4,26 @@
   Trace Processor:
     *
   UI:
-    *
+    * Add tracks to the list of searchable items.
+    * Use mipmaps to improve track query performance on large traces.
+    * Fix slow scrolling bug in ftrace explorer tab on low DPI machines.
+    * Overhaul track decider queries to improve trace load times.
+    * Add track
+    * Tidy up command names & remove some example ones.
+    * Remove arg auto-completion in pivot table.
+    * Show dominator tree views by default.
+    * Fix counter event selection off-by-one error.
+    * Add viewport control to the plugin API.
+    * Sticky track titles to improve track button accessibility in tall tracks.
+    * A handful of small bugfixes.
   SDK:
-    *
+    * The TRACE_EVENT macro used to reject `const char *` event names: either
+      `StaticString` or `DynamicString` needed to be specified. In the last year
+      (since https://r.android.com/2494614), TRACE_EVENT mistakenly accepted
+      `const char *` as an event name. This has been fixed now. In case your
+      code fails to compile,
+      https://perfetto.dev/docs/instrumentation/track-events#dynamic-event-names
+      explains how to fix the problem.
 
 
 v44.0 - 2024-04-10:
diff --git a/OWNERS b/OWNERS
index 3c29a20..b2a1331 100644
--- a/OWNERS
+++ b/OWNERS
@@ -7,7 +7,6 @@
 skyostil@google.com
 
 # UI, Ftrace interop, traced_probes, protozero, Android internals.
-hjd@google.com
 
 # Trace Processor, metrics, infra.
 lalitm@google.com
diff --git a/bazel/proto_gen.bzl b/bazel/proto_gen.bzl
index cd31d44..c5fd127 100644
--- a/bazel/proto_gen.bzl
+++ b/bazel/proto_gen.bzl
@@ -26,6 +26,11 @@
         for dep in ctx.attr.deps
         for f in dep[ProtoInfo].transitive_imports.to_list()
     ]
+    proto_paths = [
+        f
+        for dep in ctx.attr.deps
+        for f in dep[ProtoInfo].transitive_proto_path.to_list()
+    ]
 
     proto_path = "."
 
@@ -58,8 +63,10 @@
         out_files += [ctx.actions.declare_file(base_path + ".%s.cc" % suffix)]
 
     arguments = [
-        "--proto_path=" + proto_path,
+        "--proto_path=" + proto_path
+        for proto_path in proto_paths
     ]
+
     plugin_deps = []
     if ctx.attr.plugin:
         wrap_arg = ctx.attr.wrapper_namespace
diff --git a/docs/contributing/build-instructions.md b/docs/contributing/build-instructions.md
index c192898..85025d6 100644
--- a/docs/contributing/build-instructions.md
+++ b/docs/contributing/build-instructions.md
@@ -535,6 +535,7 @@
     "./ui",
   ],
   "prettier.configPath": "ui/.prettierrc.yml",
+  "typescript.preferences.importModuleSpecifier": "relative",
   "[typescript]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 09fa88e..9b1535a 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -36,9 +36,10 @@
 - Commands should have ids with the pattern `example.com#DoSomething`
 - Command's ids should be prefixed with the id of the plugin which
   provides them.
-- Commands names should have the form "Verb something something".
-  Good: "Pin janky frame timeline tracks"
-  Bad: "Tracks are Displayed if Janky"
+- Command names should have the form "Verb something something", and should be
+  in normal sentence case. I.e. don't capitalize the first letter of each word.
+  - Good: "Pin janky frame timeline tracks"
+  - Bad: "Tracks are Displayed if Janky"
 
 ### Start the dev server
 ```sh
diff --git a/docs/data-sources/java-heap-profiler.md b/docs/data-sources/java-heap-profiler.md
index 01af1f5..4056436 100644
--- a/docs/data-sources/java-heap-profiler.md
+++ b/docs/data-sources/java-heap-profiler.md
@@ -68,9 +68,9 @@
 select distinct graph_sample_ts, upid from heap_graph_object
 ```
 
-graph_sample_ts     |        upid        |
---------------------|--------------------|
-     56785646801    |         1          |
+|graph_sample_ts     |        upid        |
+|--------------------|--------------------|
+|     56785646801    |         1          |
 
 We can then use them to get the flamegraph data.
 
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index f69d3ce..a7a77c6 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -371,7 +371,7 @@
 
         metadata = {
           proto_library_sources = invoker.sources
-          import_dirs = import_dirs_
+          proto_import_dirs = import_dirs_
           exports = get_path_info(public_deps_, "abspath")
         }
         forward_variables_from(invoker, vars_to_forward)
diff --git a/gn/standalone/proto_library.gni b/gn/standalone/proto_library.gni
index 07b8140..1a23a97 100644
--- a/gn/standalone/proto_library.gni
+++ b/gn/standalone/proto_library.gni
@@ -170,6 +170,10 @@
       ]
     }
 
+    metadata = {
+      proto_import_dirs = import_dirs
+    }
+
     if (generate_cc) {
       cc_generator_options_ = ""
       if (defined(invoker.cc_generator_options)) {
diff --git a/include/perfetto/tracing/internal/track_event_macros.h b/include/perfetto/tracing/internal/track_event_macros.h
index 3e64082..2c2ddb4 100644
--- a/include/perfetto/tracing/internal/track_event_macros.h
+++ b/include/perfetto/tracing/internal/track_event_macros.h
@@ -125,7 +125,10 @@
             category)) {                                                       \
       tns::TrackEvent::CallIfEnabled(                                          \
           [&](uint32_t instances) PERFETTO_NO_THREAD_SAFETY_ANALYSIS {         \
-            tns::TrackEvent::method(instances, category, name, ##__VA_ARGS__); \
+            tns::TrackEvent::method(                                           \
+                instances, category,                                           \
+                ::perfetto::internal::DecayEventNameType(name),                \
+                ##__VA_ARGS__);                                                \
           });                                                                  \
     } else {                                                                   \
       tns::TrackEvent::CallIfCategoryEnabled(                                  \
@@ -135,7 +138,8 @@
                 instances,                                                     \
                 PERFETTO_UID(                                                  \
                     kCatIndex_ADD_TO_PERFETTO_DEFINE_CATEGORIES_IF_FAILS_),    \
-                name, ##__VA_ARGS__);                                          \
+                ::perfetto::internal::DecayEventNameType(name),                \
+                ##__VA_ARGS__);                                                \
           });                                                                  \
     }                                                                          \
   } while (false)
diff --git a/include/perfetto/tracing/track_event.h b/include/perfetto/tracing/track_event.h
index 30e04a0..872a29f 100644
--- a/include/perfetto/tracing/track_event.h
+++ b/include/perfetto/tracing/track_event.h
@@ -351,10 +351,9 @@
 //   TRACE_EVENT("category", "Name", perfetto::Track(1234),
 //               "arg", value, "arg2", value2);
 //
-#define TRACE_EVENT_BEGIN(category, name, ...)        \
-  PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(          \
-      TraceForCategory, category,                     \
-      ::perfetto::internal::DecayEventNameType(name), \
+#define TRACE_EVENT_BEGIN(category, name, ...) \
+  PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(   \
+      TraceForCategory, category, name,        \
       ::perfetto::protos::pbzero::TrackEvent::TYPE_SLICE_BEGIN, ##__VA_ARGS__)
 
 // End a slice under |category|.
@@ -365,14 +364,12 @@
 
 // Begin a slice which gets automatically closed when going out of scope.
 #define TRACE_EVENT(category, name, ...) \
-  PERFETTO_INTERNAL_SCOPED_TRACK_EVENT(  \
-      category, ::perfetto::internal::DecayEventNameType(name), ##__VA_ARGS__)
+  PERFETTO_INTERNAL_SCOPED_TRACK_EVENT(category, name, ##__VA_ARGS__)
 
 // Emit a slice which has zero duration.
-#define TRACE_EVENT_INSTANT(category, name, ...)      \
-  PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(          \
-      TraceForCategory, category,                     \
-      ::perfetto::internal::DecayEventNameType(name), \
+#define TRACE_EVENT_INSTANT(category, name, ...) \
+  PERFETTO_INTERNAL_TRACK_EVENT_WITH_METHOD(     \
+      TraceForCategory, category, name,          \
       ::perfetto::protos::pbzero::TrackEvent::TYPE_INSTANT, ##__VA_ARGS__)
 
 // Efficiently determine if the given static or dynamic trace category or
diff --git a/infra/OWNERS b/infra/OWNERS
index a4b3fef..0178ed0 100644
--- a/infra/OWNERS
+++ b/infra/OWNERS
@@ -1,3 +1,2 @@
-hjd@google.com
 lalitm@google.com
 primiano@google.com
diff --git a/protos/perfetto/config/android/BUILD.gn b/protos/perfetto/config/android/BUILD.gn
index 0af8024..ec4f7ce 100644
--- a/protos/perfetto/config/android/BUILD.gn
+++ b/protos/perfetto/config/android/BUILD.gn
@@ -26,6 +26,7 @@
     "android_system_property_config.proto",
     "network_trace_config.proto",
     "packages_list_config.proto",
+    "pixel_modem_config.proto",
     "protolog_config.proto",
     "surfaceflinger_layers_config.proto",
     "surfaceflinger_transactions_config.proto",
diff --git a/protos/perfetto/config/android/pixel_modem_config.proto b/protos/perfetto/config/android/pixel_modem_config.proto
new file mode 100644
index 0000000..d3efc59
--- /dev/null
+++ b/protos/perfetto/config/android/pixel_modem_config.proto
@@ -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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// Data source that records events from the modem.
+message PixelModemConfig {
+  // Event group to record, as defined by the modem.
+  enum EventGroup {
+    EVENT_GROUP_UNKNOWN = 0;
+
+    // Events suitable for low bandwidth tracing only.
+    EVENT_GROUP_LOW_BANDWIDTH = 1;
+
+    // Events suitable for high and low bandwidth tracing.
+    EVENT_GROUP_HIGH_AND_LOW_BANDWIDTH = 2;
+  }
+
+  optional EventGroup event_group = 1;
+
+  // If set, record only events with these hashes.
+  repeated int64 pigweed_hash_allow_list = 2;
+
+  // If set and allow_list is not set, deny events with these hashes.
+  repeated int64 pigweed_hash_deny_list = 3;
+}
diff --git a/protos/perfetto/config/data_source_config.proto b/protos/perfetto/config/data_source_config.proto
index e9a2594..3b8e860 100644
--- a/protos/perfetto/config/data_source_config.proto
+++ b/protos/perfetto/config/data_source_config.proto
@@ -26,6 +26,7 @@
 import "protos/perfetto/config/android/android_sdk_sysprop_guard_config.proto";
 import "protos/perfetto/config/android/network_trace_config.proto";
 import "protos/perfetto/config/android/packages_list_config.proto";
+import "protos/perfetto/config/android/pixel_modem_config.proto";
 import "protos/perfetto/config/android/protolog_config.proto";
 import "protos/perfetto/config/android/surfaceflinger_layers_config.proto";
 import "protos/perfetto/config/android/surfaceflinger_transactions_config.proto";
@@ -49,7 +50,7 @@
 import "protos/perfetto/config/system_info/system_info.proto";
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 129
+// Next id: 130
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -197,6 +198,9 @@
   // Data source name: android.input.inputevent
   optional AndroidInputEventConfig android_input_event_config = 128 [lazy = true];
 
+  // Data source name: android.pixel.modem
+  optional PixelModemConfig pixel_modem_config = 129 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 2fd3c55..7777d1c 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -660,6 +660,32 @@
 
 // End of protos/perfetto/config/android/packages_list_config.proto
 
+// Begin of protos/perfetto/config/android/pixel_modem_config.proto
+
+// Data source that records events from the modem.
+message PixelModemConfig {
+  // Event group to record, as defined by the modem.
+  enum EventGroup {
+    EVENT_GROUP_UNKNOWN = 0;
+
+    // Events suitable for low bandwidth tracing only.
+    EVENT_GROUP_LOW_BANDWIDTH = 1;
+
+    // Events suitable for high and low bandwidth tracing.
+    EVENT_GROUP_HIGH_AND_LOW_BANDWIDTH = 2;
+  }
+
+  optional EventGroup event_group = 1;
+
+  // If set, record only events with these hashes.
+  repeated int64 pigweed_hash_allow_list = 2;
+
+  // If set and allow_list is not set, deny events with these hashes.
+  repeated int64 pigweed_hash_deny_list = 3;
+}
+
+// End of protos/perfetto/config/android/pixel_modem_config.proto
+
 // Begin of protos/perfetto/common/protolog_common.proto
 
 enum ProtoLogLevel {
@@ -3231,7 +3257,7 @@
 // Begin of protos/perfetto/config/data_source_config.proto
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 129
+// Next id: 130
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -3379,6 +3405,9 @@
   // Data source name: android.input.inputevent
   optional AndroidInputEventConfig android_input_event_config = 128 [lazy = true];
 
+  // Data source name: android.pixel.modem
+  optional PixelModemConfig pixel_modem_config = 129 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
diff --git a/protos/perfetto/ipc/OWNERS b/protos/perfetto/ipc/OWNERS
index a10c8b8..bead02d 100644
--- a/protos/perfetto/ipc/OWNERS
+++ b/protos/perfetto/ipc/OWNERS
@@ -4,6 +4,5 @@
 
 set noparent
 eseckler@google.com
-hjd@google.com
 primiano@google.com
 skyostil@google.com
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 5b8fab6..5430c22 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -28,6 +28,7 @@
     "android_boot_unagg.proto",
     "android_frame_timeline_metric.proto",
     "android_garbage_collection_unagg_metric.proto",
+    "android_oom_adjuster_metric.proto",
     "android_sysui_notifications_blocking_calls_metric.proto",
     "android_trusty_workqueues.proto",
     "anr_metric.proto",
diff --git a/protos/perfetto/metrics/android/android_boot.proto b/protos/perfetto/metrics/android/android_boot.proto
index a043efe..9f90ba5 100644
--- a/protos/perfetto/metrics/android/android_boot.proto
+++ b/protos/perfetto/metrics/android/android_boot.proto
@@ -56,8 +56,39 @@
     optional int64 alloc_gc_count = 11;
     optional double mb_per_ms_of_gc = 12;
   }
+  message OomAdjusterTransitionCounts {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason.
+    optional string name = 1;
+    // name of previous oom bucket.
+    optional string src_bucket = 2;
+    // name of oom bucket.
+    optional string dest_bucket = 3;
+    // count of transitions
+    optional int64 count = 4;
+  }
+  message OomAdjBucketDurationAggregation {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason
+    optional string name = 1;
+    // name of oom bucket.
+    optional string bucket = 2;
+    // Duration of the time in the bucket
+    optional int64 total_dur = 3;
+  }
+  message OomAdjDurationAggregation {
+    optional int64 min_oom_adj_dur = 1;
+    optional int64 max_oom_adj_dur = 2;
+    optional double avg_oom_adj_dur = 3;
+    optional int64 oom_adj_event_count = 4;
+    optional string oom_adj_reason = 5;
+  }
   optional ProcessStartAggregation full_trace_process_start_aggregation = 6;
   optional ProcessStartAggregation post_boot_process_start_aggregation = 7;
   optional GarbageCollectionAggregation full_trace_gc_aggregation = 8;
   optional GarbageCollectionAggregation post_boot_gc_aggregation = 9;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_global = 10;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_by_process = 11;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_by_oom_adj_reason = 12;
+  repeated OomAdjBucketDurationAggregation post_boot_oom_adj_bucket_duration_agg_global = 13;
+  repeated OomAdjBucketDurationAggregation post_boot_oom_adj_bucket_duration_agg_by_process = 14;
+  repeated OomAdjDurationAggregation post_boot_oom_adj_duration_agg = 15;
 }
diff --git a/protos/perfetto/metrics/android/android_oom_adjuster_metric.proto b/protos/perfetto/metrics/android/android_oom_adjuster_metric.proto
new file mode 100644
index 0000000..95af7b4
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_oom_adjuster_metric.proto
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+message AndroidOomAdjusterMetric {
+  message OomAdjusterTransitionCounts {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason.
+    optional string name = 1;
+    // name of previous oom bucket.
+    optional string src_bucket = 2;
+    // name of oom bucket.
+    optional string dest_bucket = 3;
+    // count of transitions
+    optional int64 count = 4;
+  }
+  message OomAdjBucketDurationAggregation {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason
+    optional string name = 1;
+    // name of oom bucket.
+    optional string bucket = 2;
+    // Duration of the time in the bucket
+    optional int64 total_dur = 3;
+  }
+  message OomAdjDurationAggregation {
+    optional int64 min_oom_adj_dur = 1;
+    optional int64 max_oom_adj_dur = 2;
+    optional double avg_oom_adj_dur = 3;
+    optional int64 oom_adj_event_count = 4;
+    optional string oom_adj_reason = 5;
+  }
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_global = 1;
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_by_process = 2;
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_by_oom_adj_reason = 3;
+  repeated OomAdjBucketDurationAggregation oom_adj_bucket_duration_agg_global = 4;
+  repeated OomAdjBucketDurationAggregation oom_adj_bucket_duration_agg_by_process = 5;
+  repeated OomAdjDurationAggregation oom_adj_duration_agg = 6;
+}
\ No newline at end of file
diff --git a/protos/perfetto/metrics/android/java_heap_stats.proto b/protos/perfetto/metrics/android/java_heap_stats.proto
index 10a0b8b..5163f41 100644
--- a/protos/perfetto/metrics/android/java_heap_stats.proto
+++ b/protos/perfetto/metrics/android/java_heap_stats.proto
@@ -26,7 +26,7 @@
     optional int64 obj_count = 3;
   }
 
-  // Next id: 11
+  // Next id: 12
   message Sample {
     optional int64 ts = 1;
     // Size of the Java heap in bytes
@@ -47,6 +47,8 @@
     repeated HeapRoots roots = 7;
     // OOM adjustment score
     optional int64 oom_score_adj = 10;
+    // Process uptime in millis at the time of the heap dump
+    optional int64 process_uptime_ms = 11;
   }
 
   // Heap stats per process. One sample per dump (can be > 1 if continuous
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 83395ec..c3025c3 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -71,6 +71,7 @@
 import "protos/perfetto/metrics/android/monitor_contention_metric.proto";
 import "protos/perfetto/metrics/android/monitor_contention_agg_metric.proto";
 import "protos/perfetto/metrics/android/app_process_starts_metric.proto";
+import "protos/perfetto/metrics/android/android_oom_adjuster_metric.proto";
 
 // Trace processor metadata
 message TraceMetadata {
@@ -298,6 +299,10 @@
   // All blocking calls (e.g. binder calls) for a trace.
   optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
 
+  // Android OOM unaggregated metrics.
+  optional AndroidOomAdjusterMetric android_oom_adjuster = 66;
+
+  // Android
   // 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 14a2847..86f7d59 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -203,10 +203,41 @@
     optional int64 alloc_gc_count = 11;
     optional double mb_per_ms_of_gc = 12;
   }
+  message OomAdjusterTransitionCounts {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason.
+    optional string name = 1;
+    // name of previous oom bucket.
+    optional string src_bucket = 2;
+    // name of oom bucket.
+    optional string dest_bucket = 3;
+    // count of transitions
+    optional int64 count = 4;
+  }
+  message OomAdjBucketDurationAggregation {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason
+    optional string name = 1;
+    // name of oom bucket.
+    optional string bucket = 2;
+    // Duration of the time in the bucket
+    optional int64 total_dur = 3;
+  }
+  message OomAdjDurationAggregation {
+    optional int64 min_oom_adj_dur = 1;
+    optional int64 max_oom_adj_dur = 2;
+    optional double avg_oom_adj_dur = 3;
+    optional int64 oom_adj_event_count = 4;
+    optional string oom_adj_reason = 5;
+  }
   optional ProcessStartAggregation full_trace_process_start_aggregation = 6;
   optional ProcessStartAggregation post_boot_process_start_aggregation = 7;
   optional GarbageCollectionAggregation full_trace_gc_aggregation = 8;
   optional GarbageCollectionAggregation post_boot_gc_aggregation = 9;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_global = 10;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_by_process = 11;
+  repeated OomAdjusterTransitionCounts post_boot_oom_adjuster_transition_counts_by_oom_adj_reason = 12;
+  repeated OomAdjBucketDurationAggregation post_boot_oom_adj_bucket_duration_agg_global = 13;
+  repeated OomAdjBucketDurationAggregation post_boot_oom_adj_bucket_duration_agg_by_process = 14;
+  repeated OomAdjDurationAggregation post_boot_oom_adj_duration_agg = 15;
 }
 
 // End of protos/perfetto/metrics/android/android_boot.proto
@@ -365,6 +396,43 @@
 
 // End of protos/perfetto/metrics/android/android_frame_timeline_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_oom_adjuster_metric.proto
+
+message AndroidOomAdjusterMetric {
+  message OomAdjusterTransitionCounts {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason.
+    optional string name = 1;
+    // name of previous oom bucket.
+    optional string src_bucket = 2;
+    // name of oom bucket.
+    optional string dest_bucket = 3;
+    // count of transitions
+    optional int64 count = 4;
+  }
+  message OomAdjBucketDurationAggregation {
+    // name of the item aggregated by. example: process_name, oom_adjuster_reason
+    optional string name = 1;
+    // name of oom bucket.
+    optional string bucket = 2;
+    // Duration of the time in the bucket
+    optional int64 total_dur = 3;
+  }
+  message OomAdjDurationAggregation {
+    optional int64 min_oom_adj_dur = 1;
+    optional int64 max_oom_adj_dur = 2;
+    optional double avg_oom_adj_dur = 3;
+    optional int64 oom_adj_event_count = 4;
+    optional string oom_adj_reason = 5;
+  }
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_global = 1;
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_by_process = 2;
+  repeated OomAdjusterTransitionCounts oom_adjuster_transition_counts_by_oom_adj_reason = 3;
+  repeated OomAdjBucketDurationAggregation oom_adj_bucket_duration_agg_global = 4;
+  repeated OomAdjBucketDurationAggregation oom_adj_bucket_duration_agg_by_process = 5;
+  repeated OomAdjDurationAggregation oom_adj_duration_agg = 6;
+}
+// End of protos/perfetto/metrics/android/android_oom_adjuster_metric.proto
+
 // Begin of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
 
 // Blocking calls inside System UI Notifications. Shows count and total duration for each.
@@ -1417,7 +1485,7 @@
     optional int64 obj_count = 3;
   }
 
-  // Next id: 11
+  // Next id: 12
   message Sample {
     optional int64 ts = 1;
     // Size of the Java heap in bytes
@@ -1438,6 +1506,8 @@
     repeated HeapRoots roots = 7;
     // OOM adjustment score
     optional int64 oom_score_adj = 10;
+    // Process uptime in millis at the time of the heap dump
+    optional int64 process_uptime_ms = 11;
   }
 
   // Heap stats per process. One sample per dump (can be > 1 if continuous
@@ -2719,6 +2789,10 @@
   // All blocking calls (e.g. binder calls) for a trace.
   optional AndroidBlockingCallsUnagg android_blocking_calls_unagg = 65;
 
+  // Android OOM unaggregated metrics.
+  optional AndroidOomAdjusterMetric android_oom_adjuster = 66;
+
+  // Android
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/trace/android/BUILD.gn b/protos/perfetto/trace/android/BUILD.gn
index c88ca28..1910078 100644
--- a/protos/perfetto/trace/android/BUILD.gn
+++ b/protos/perfetto/trace/android/BUILD.gn
@@ -29,6 +29,7 @@
     "initial_display_state.proto",
     "network_trace.proto",
     "packages_list.proto",
+    "pixel_modem_events.proto",
     "protolog.proto",
     "shell_transition.proto",
     "surfaceflinger_common.proto",
diff --git a/protos/perfetto/trace/android/pixel_modem_events.proto b/protos/perfetto/trace/android/pixel_modem_events.proto
new file mode 100644
index 0000000..f014311
--- /dev/null
+++ b/protos/perfetto/trace/android/pixel_modem_events.proto
@@ -0,0 +1,36 @@
+/*
+ * 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
+ *
+ *      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;
+
+message PixelModemEvents {
+  // Pigweed-format dehydrated events.
+  repeated bytes events = 1;
+
+  // Timestamps of the events, converted to CLOCK_BOOTTIME. The first
+  // timestamp is the absolute timestamp of the first event. Subsequent
+  // timestamps are deltas from the previous timestamp.
+  // The nth entry from `events` gets the nth entry here.
+  repeated uint64 event_time_nanos = 2;
+}
+
+// NB: this is not emitted in the trace but can be prepended later.
+message PixelModemTokenDatabase {
+  // Pigweed-format database to allow event rehydration.
+  optional bytes database = 1;
+}
diff --git a/protos/perfetto/trace/chrome/chrome_trigger.proto b/protos/perfetto/trace/chrome/chrome_trigger.proto
index 9662e92..4ba021a 100644
--- a/protos/perfetto/trace/chrome/chrome_trigger.proto
+++ b/protos/perfetto/trace/chrome/chrome_trigger.proto
@@ -17,12 +17,12 @@
 
 package perfetto.protos;
 
-// When a TracingSession receives a trigger it records the boot time
-// nanoseconds in the TracePacket's timestamp field. We emit this data so
-// filtering can be done on triggers received in the trace.
+// Information about a specific trigger during a background tracing scenario
+// Associated packet timestamps are useful to delimitate a scenario range in a
+// trace. Triggers are also useful for filtering traces.
 message ChromeTrigger {
   // Name of the trigger which was received.
   optional string trigger_name = 1;
-  // MD5 hash of the trigger name.
-  optional string trigger_hash = 2;
+  // SHA1 hash of the trigger name.
+  optional fixed32 trigger_name_hash = 2;
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index a821823..81977bf 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -660,6 +660,32 @@
 
 // End of protos/perfetto/config/android/packages_list_config.proto
 
+// Begin of protos/perfetto/config/android/pixel_modem_config.proto
+
+// Data source that records events from the modem.
+message PixelModemConfig {
+  // Event group to record, as defined by the modem.
+  enum EventGroup {
+    EVENT_GROUP_UNKNOWN = 0;
+
+    // Events suitable for low bandwidth tracing only.
+    EVENT_GROUP_LOW_BANDWIDTH = 1;
+
+    // Events suitable for high and low bandwidth tracing.
+    EVENT_GROUP_HIGH_AND_LOW_BANDWIDTH = 2;
+  }
+
+  optional EventGroup event_group = 1;
+
+  // If set, record only events with these hashes.
+  repeated int64 pigweed_hash_allow_list = 2;
+
+  // If set and allow_list is not set, deny events with these hashes.
+  repeated int64 pigweed_hash_deny_list = 3;
+}
+
+// End of protos/perfetto/config/android/pixel_modem_config.proto
+
 // Begin of protos/perfetto/common/protolog_common.proto
 
 enum ProtoLogLevel {
@@ -3231,7 +3257,7 @@
 // Begin of protos/perfetto/config/data_source_config.proto
 
 // The configuration that is passed to each data source when starting tracing.
-// Next id: 129
+// Next id: 130
 message DataSourceConfig {
   enum SessionInitiator {
     SESSION_INITIATOR_UNSPECIFIED = 0;
@@ -3379,6 +3405,9 @@
   // Data source name: android.input.inputevent
   optional AndroidInputEventConfig android_input_event_config = 128 [lazy = true];
 
+  // Data source name: android.pixel.modem
+  optional PixelModemConfig pixel_modem_config = 129 [lazy = true];
+
   // This is a fallback mechanism to send a free-form text config to the
   // producer. In theory this should never be needed. All the code that
   // is part of the platform (i.e. traced service) is supposed to *not* truncate
@@ -4996,6 +5025,27 @@
 
 // End of protos/perfetto/trace/android/packages_list.proto
 
+// Begin of protos/perfetto/trace/android/pixel_modem_events.proto
+
+message PixelModemEvents {
+  // Pigweed-format dehydrated events.
+  repeated bytes events = 1;
+
+  // Timestamps of the events, converted to CLOCK_BOOTTIME. The first
+  // timestamp is the absolute timestamp of the first event. Subsequent
+  // timestamps are deltas from the previous timestamp.
+  // The nth entry from `events` gets the nth entry here.
+  repeated uint64 event_time_nanos = 2;
+}
+
+// NB: this is not emitted in the trace but can be prepended later.
+message PixelModemTokenDatabase {
+  // Pigweed-format database to allow event rehydration.
+  optional bytes database = 1;
+}
+
+// End of protos/perfetto/trace/android/pixel_modem_events.proto
+
 // Begin of protos/perfetto/trace/android/protolog.proto
 
 /* represents a single log entry */
@@ -6000,14 +6050,14 @@
 
 // Begin of protos/perfetto/trace/chrome/chrome_trigger.proto
 
-// When a TracingSession receives a trigger it records the boot time
-// nanoseconds in the TracePacket's timestamp field. We emit this data so
-// filtering can be done on triggers received in the trace.
+// Information about a specific trigger during a background tracing scenario
+// Associated packet timestamps are useful to delimitate a scenario range in a
+// trace. Triggers are also useful for filtering traces.
 message ChromeTrigger {
   // Name of the trigger which was received.
   optional string trigger_name = 1;
-  // MD5 hash of the trigger name.
-  optional string trigger_hash = 2;
+  // SHA1 hash of the trigger name.
+  optional fixed32 trigger_name_hash = 2;
 }
 
 // End of protos/perfetto/trace/chrome/chrome_trigger.proto
@@ -14721,7 +14771,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 109.
+// Next id: 112.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -14861,6 +14911,9 @@
     // Clock synchronization with remote machines.
     RemoteClockSync remote_clock_sync = 107;
 
+    PixelModemEvents pixel_modem_events = 110;
+    PixelModemTokenDatabase pixel_modem_token_database = 111;
+
     // This field is only used for testing.
     // In previous versions of this proto this field had the id 268435455
     // This caused many problems:
diff --git a/protos/perfetto/trace/trace_packet.proto b/protos/perfetto/trace/trace_packet.proto
index 93fb7d3..62d7e6f 100644
--- a/protos/perfetto/trace/trace_packet.proto
+++ b/protos/perfetto/trace/trace_packet.proto
@@ -30,6 +30,7 @@
 import "protos/perfetto/trace/android/initial_display_state.proto";
 import "protos/perfetto/trace/android/network_trace.proto";
 import "protos/perfetto/trace/android/packages_list.proto";
+import "protos/perfetto/trace/android/pixel_modem_events.proto";
 import "protos/perfetto/trace/android/protolog.proto";
 import "protos/perfetto/trace/android/shell_transition.proto";
 import "protos/perfetto/trace/android/surfaceflinger_layers.proto";
@@ -103,7 +104,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 109.
+// Next id: 112.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -243,6 +244,9 @@
     // Clock synchronization with remote machines.
     RemoteClockSync remote_clock_sync = 107;
 
+    PixelModemEvents pixel_modem_events = 110;
+    PixelModemTokenDatabase pixel_modem_token_database = 111;
+
     // This field is only used for testing.
     // In previous versions of this proto this field had the id 268435455
     // This caused many problems:
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index 882d6ff..0a8842f 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -303,6 +303,9 @@
   // Number of "pending reuse" references active in the RenderProcessHost,
   // recorded when Cleanup() was called.
   optional uint32 pending_reuse_ref_count = 5;
+  // Number of NavigationStateKeepAlive references active in the
+  // RenderProcessHost, recorded when Cleanup() was called.
+  optional uint32 navigation_state_keepalive_count = 6;
 }
 
 message ChildProcessLauncherPriority {
@@ -596,7 +599,7 @@
   optional ShutdownBehavior shutdown_behavior = 4;
 }
 
-// TODO(crbug.com/1258495): Add more information.
+// TODO(crbug.com/40797026): Add more information.
 message BackForwardCacheCanStoreDocumentResult {
   enum BackForwardCacheNotRestoredReason {
     NOT_MAIN_FRAME = 1;
@@ -722,7 +725,7 @@
     TASK_TYPE_WORKER_THREAD_TASK_QUEUE_COMPOSITOR = 48;
     TASK_TYPE_COMPOSITOR_THREAD_TASK_QUEUE_INPUT = 49;
 
-    // TODO(crbug.com/860545): Obsolete. Remove.
+    // TODO(crbug.com/40583778): Obsolete. Remove.
     TASK_TYPE_NETWORKING_WITH_URL_LOADER_ANNOTATION = 50;
 
     TASK_TYPE_WORKER_ANIMATION = 51;
@@ -1835,9 +1838,27 @@
   optional int64 draw_estimate_delta_us = 7;
 }
 
+message WebViewStartup {
+  optional bool from_ui_thread = 1;
+  // This enum must be kept in sync with WebViewChromiumAwInit.CallSite
+  enum CallSite {
+    GET_AW_TRACING_CONTROLLER = 0;
+    GET_AW_PROXY_CONTROLLER = 1;
+    WEBVIEW_INSTANCE = 2;
+    GET_STATICS = 3;
+    GET_DEFAULT_GEOLOCATION_PERMISSIONS = 4;
+    GET_DEFAULT_SERVICE_WORKER_CONTROLLER = 5;
+    GET_WEB_ICON_DATABASE = 6;
+    GET_DEFAULT_WEB_STORAGE = 7;
+    GET_DEFAULT_WEBVIEW_DATABASE = 8;
+    GET_TRACING_CONTROLLER = 9;
+  }
+  optional CallSite call_site = 2;
+}
+
 message ChromeTrackEvent {
   // Extension range for Chrome: 1000-1999
-  // Next ID: 1063
+  // Next ID: 1064
   extend TrackEvent {
     optional ChromeAppState chrome_app_state = 1000;
 
@@ -1970,5 +1991,7 @@
     optional ViewClassName view_class_name = 1061;
 
     optional ChromeCompositorSchedulerStateV2 cc_scheduler_state = 1062;
+
+    optional WebViewStartup webview_startup = 1063;
   }
 }
diff --git a/python/generators/diff_tests/runner.py b/python/generators/diff_tests/runner.py
index b3c9fe9..7c77d54 100644
--- a/python/generators/diff_tests/runner.py
+++ b/python/generators/diff_tests/runner.py
@@ -28,7 +28,8 @@
 from python.generators.diff_tests.testing import TestCase, TestType, BinaryProto
 from python.generators.diff_tests.utils import (
     ColorFormatter, create_message_factory, get_env, get_trace_descriptor_path,
-    read_all_tests, serialize_python_trace, serialize_textproto_trace)
+    read_all_tests, serialize_python_trace, serialize_textproto_trace,
+    modify_trace)
 
 ROOT_DIR = os.path.dirname(
     os.path.dirname(
@@ -327,6 +328,19 @@
       with open(gen_trace_file.name, 'w') as trace_file:
         trace_file.write(self.test.blueprint.trace.contents)
 
+    if self.test.blueprint.trace_modifier is not None:
+      if gen_trace_file:
+        # Overwrite |gen_trace_file|.
+        modify_trace(self.trace_descriptor_path, extension_descriptor_paths,
+                     gen_trace_file.name, gen_trace_file.name,
+                     self.test.blueprint.trace_modifier)
+      else:
+        # Create |gen_trace_file| to save the modified trace.
+        gen_trace_file = tempfile.NamedTemporaryFile(delete=False)
+        modify_trace(self.trace_descriptor_path, extension_descriptor_paths,
+                     self.test.trace_path, gen_trace_file.name,
+                     self.test.blueprint.trace_modifier)
+
     if gen_trace_file:
       trace_path = os.path.realpath(gen_trace_file.name)
     else:
diff --git a/python/generators/diff_tests/testing.py b/python/generators/diff_tests/testing.py
index 8a4f45e..515f956 100644
--- a/python/generators/diff_tests/testing.py
+++ b/python/generators/diff_tests/testing.py
@@ -16,7 +16,7 @@
 import inspect
 import os
 from dataclasses import dataclass
-from typing import List, Union, Callable
+from typing import Any, Dict, List, Union, Callable
 from enum import Enum
 import re
 
@@ -72,6 +72,42 @@
   contents: str
 
 
+class TraceInjector:
+  '''Injects fields into trace packets before test starts.
+
+  TraceInjector can be used within a DiffTestBlueprint to selectively inject
+  fields to trace packets containing specific data types. For example:
+
+    DiffTestBlueprint(
+        trace=...,
+        trace_modifier=TraceInjector('ftrace_events',
+                                     'sys_stats',
+                                     'process_tree',
+                                     {'machine_id': 1001},
+                                     trusted_uid=123)
+        query=...,
+        out=...)
+
+  packet_data_types: Data types to target for injection ('ftrace_events',
+  'sys_stats', 'process_tree')
+  injected_fields: Fields and their values to inject into matching packets
+  ({'machine_id': 1001}, trusted_uid=123).
+  '''
+
+  def __init__(self, packet_data_types: List[str], injected_fields: Dict[str,
+                                                                         Any]):
+    self.packet_data_types = packet_data_types
+    self.injected_fields = injected_fields
+
+  def inject(self, proto):
+    for p in proto.packet:
+      for f in self.packet_data_types:
+        if p.HasField(f):
+          for k, v, in self.injected_fields.items():
+            setattr(p, k, v)
+          continue
+
+
 class TestType(Enum):
   QUERY = 1
   METRIC = 2
@@ -86,6 +122,7 @@
   trace: Union[Path, DataPath, Json, Systrace, TextProto]
   query: Union[str, Path, DataPath, Metric]
   out: Union[Path, DataPath, Json, Csv, TextProto, BinaryProto]
+  trace_modifier: Union[TraceInjector, None] = None
 
   def is_trace_file(self):
     return isinstance(self.trace, Path)
diff --git a/python/generators/diff_tests/utils.py b/python/generators/diff_tests/utils.py
index cb447fd..3101637 100644
--- a/python/generators/diff_tests/utils.py
+++ b/python/generators/diff_tests/utils.py
@@ -149,6 +149,23 @@
   return trace_descriptor_path
 
 
+def modify_trace(trace_descriptor_path, extension_descriptor_paths,
+                 in_trace_path, out_trace_path, modifier):
+  trace_proto = create_message_factory([trace_descriptor_path] +
+                                       extension_descriptor_paths,
+                                       'perfetto.protos.Trace')()
+
+  with open(in_trace_path, "rb") as f:
+    # This may raise DecodeError when |in_trace_path| isn't protobuf.
+    trace_proto.ParseFromString(f.read())
+    # Modify the trace proto object with the provided modifier function.
+    modifier.inject(trace_proto)
+
+  with open(out_trace_path, "wb") as f:
+    f.write(trace_proto.SerializeToString())
+    f.flush()
+
+
 def read_all_tests(name_filter: str, root_dir: str) -> List[testing.TestCase]:
   # Import
   INCLUDE_PATH = os.path.join(root_dir, 'test', 'trace_processor', 'diff_tests')
diff --git a/python/generators/trace_processor_table/serialize.py b/python/generators/trace_processor_table/serialize.py
index b9d5170..1e613e3 100644
--- a/python/generators/trace_processor_table/serialize.py
+++ b/python/generators/trace_processor_table/serialize.py
@@ -674,15 +674,13 @@
 
   Iterator IterateRows() {{ return Iterator(this, Table::IterateRows()); }}
 
-  ConstIterator FilterToIterator(
-      const std::vector<Constraint>& cs) const {{
+  ConstIterator FilterToIterator(const Query& q) const {{
     return ConstIterator(
-      this, ApplyAndIterateRows(QueryToRowMap(cs, {{}})));
+      this, ApplyAndIterateRows(QueryToRowMap(q)));
   }}
 
-  Iterator FilterToIterator(
-      const std::vector<Constraint>& cs) {{
-    return Iterator(this, ApplyAndIterateRows(QueryToRowMap(cs, {{}})));
+  Iterator FilterToIterator(const Query& q) {{
+    return Iterator(this, ApplyAndIterateRows(QueryToRowMap(q)));
   }}
 
   void ShrinkToFit() {{
diff --git a/python/perfetto/prebuilts/perfetto_prebuilts.py b/python/perfetto/prebuilts/perfetto_prebuilts.py
index 087a337..61283cc 100644
--- a/python/perfetto/prebuilts/perfetto_prebuilts.py
+++ b/python/perfetto/prebuilts/perfetto_prebuilts.py
@@ -46,6 +46,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -62,28 +65,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 6a137a9..c337e46 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_uri_resolver/resolver.py b/python/perfetto/trace_uri_resolver/resolver.py
index bcdbf93..32f224d 100644
--- a/python/perfetto/trace_uri_resolver/resolver.py
+++ b/python/perfetto/trace_uri_resolver/resolver.py
@@ -190,11 +190,13 @@
   """Creates an the args dictionary from a trace URI.
 
     URIs have the form:
-    android_ci:day=2021-01-01;devices=blueline,crosshatch;key>=value
+    android_ci:day=2021-01-01;devices=blueline,crosshatch;key>=value;\
+    version>=1;version<5
 
     This is converted to a dictionary of the form:
     {'day': '2021-01-01', 'id': ['blueline', 'crosshatch'],
-    'key': ConstraintClass('value', Op.GE)}
+    'key': ConstraintClass('value', Op.GE),
+    'version': [ConstraintClass(1, Op.GE), ConstraintClass(5, Op.LT)]}
   """
   _, args_str = util.parse_trace_uri(uri)
   if not args_str:
@@ -206,13 +208,14 @@
     (key, op, value) = _parse_arg(arg)
     lst = value.split(',')
     if len(lst) > 1:
-      args_dict[key] = lst
+      args_dict_value = lst
     else:
-      args_dict[key] = value
+      args_dict_value = value
 
     if key not in type_hints:
       if op != ConstraintClass.Op.EQ:
         raise ValueError(f'{key} only supports "=" operator')
+      args_dict[key] = args_dict_value
       continue
     have_constraint = False
     type_hint = type_hints[key]
@@ -227,5 +230,12 @@
       raise ValueError('Operator other than "=" passed to argument which '
                        'does not have constraint type: ' + arg)
     if have_constraint:
-      args_dict[key] = ConstraintClass(args_dict[key], op)
+      if key not in args_dict:
+        args_dict[key] = ConstraintClass(args_dict_value, op)
+      else:
+        if isinstance(args_dict[key], ConstraintClass):
+          args_dict[key] = [args_dict[key]]
+        args_dict[key].append(ConstraintClass(args_dict_value, op))
+    else:
+      args_dict[key] = args_dict_value
   return args_dict
diff --git a/python/test/resolver_unittest.py b/python/test/resolver_unittest.py
index 5cdb9f9..5ac9f63 100644
--- a/python/test/resolver_unittest.py
+++ b/python/test/resolver_unittest.py
@@ -197,6 +197,22 @@
     self.assertEqual(
         _args_dict_from_uri('foo:key<v1', type_hints),
         {'key': ConstraintClass('v1', ConstraintClass.Op.LT)})
+    self.assertEqual(
+        _args_dict_from_uri('foo:key>v1;key<=v2', type_hints), {
+            'key': [
+                ConstraintClass('v1', ConstraintClass.Op.GT),
+                ConstraintClass('v2', ConstraintClass.Op.LE)
+            ]
+        })
+    self.assertEqual(
+        _args_dict_from_uri('foo:key>=v1;key<v4;key!=v2;key!=v3', type_hints), {
+            'key': [
+                ConstraintClass('v1', ConstraintClass.Op.GE),
+                ConstraintClass('v4', ConstraintClass.Op.LT),
+                ConstraintClass('v2', ConstraintClass.Op.NE),
+                ConstraintClass('v3', ConstraintClass.Op.NE),
+            ]
+        })
 
   def _check_resolver_result(self,
                              foo_res,
diff --git a/src/protozero/protoc_plugin/protozero_c_plugin.cc b/src/protozero/protoc_plugin/protozero_c_plugin.cc
index 5bd05dd..804968e 100644
--- a/src/protozero/protoc_plugin/protozero_c_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_c_plugin.cc
@@ -258,8 +258,9 @@
     for (int i = 0; i < source_->enum_type_count(); ++i)
       enums_.push_back(source_->enum_type(i));
 
-    if (source_->extension_count() > 0)
-      Abort("top-level extension blocks are not supported");
+    if (source_->extension_count() > 0) {
+      // TODO(b/336524288): emit field numbers
+    }
 
     for (const Descriptor* message : messages_) {
       for (int i = 0; i < message->enum_type_count(); ++i) {
@@ -293,7 +294,6 @@
     while (!stack.empty()) {
       const FileDescriptor* imp = stack.back();
       stack.pop_back();
-
       for (int i = 0; i < imp->public_dependency_count(); ++i) {
         stack.push_back(imp->public_dependency(i));
       }
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 5b08292..26f3110 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -371,6 +371,12 @@
           // name of this message is used to group them.
           std::string extension_name = extension->extension_scope()->name();
           extensions_[extension_name].push_back(extension);
+
+          if (extension->message_type()) {
+            // Emit a forward declaration of nested message types, as the outer
+            // class will refer to them when creating type aliases.
+            referenced_messages_.insert(extension->message_type());
+          }
         }
       } else {
         messages_.push_back(message);
@@ -387,8 +393,9 @@
     for (int i = 0; i < source_->enum_type_count(); ++i)
       enums_.push_back(source_->enum_type(i));
 
-    if (source_->extension_count() > 0)
-      Abort("top-level extension blocks are not supported");
+    if (source_->extension_count() > 0) {
+      // TODO(b/336524288): emit field numbers
+    }
 
     for (const Descriptor* message : messages_) {
       for (int i = 0; i < message->enum_type_count(); ++i) {
@@ -755,6 +762,15 @@
       if (field->is_repeated() && !field->is_packed())
         has_nonpacked_repeated_fields = true;
     }
+    // Iterate over all fields in "extend" blocks.
+    for (int i = 0; i < message->extension_range_count(); ++i) {
+      Descriptor::ExtensionRange::Proto range;
+      message->extension_range(i)->CopyTo(&range);
+      int candidate = range.end() - 1;
+      if (candidate > kMaxDecoderFieldId)
+        continue;
+      max_field_id = std::max(max_field_id, candidate);
+    }
 
     std::string class_name = GetCppClassName(message) + "_Decoder";
     stub_h_->Print(
@@ -880,7 +896,8 @@
   }
 
   void GenerateConstantsForMessageFields(const Descriptor* message) {
-    const bool has_fields = (message->field_count() > 0);
+    const bool has_fields =
+        message->field_count() > 0 || message->extension_count() > 0;
 
     // Field number constants.
     if (has_fields) {
@@ -893,6 +910,15 @@
                        GetFieldNumberConstant(field), "id",
                        std::to_string(field->number()));
       }
+
+      for (int i = 0; i < message->extension_count(); ++i) {
+        const FieldDescriptor* field = message->extension(i);
+
+        stub_h_->Print("$name$ = $id$,\n", "name",
+                       GetFieldNumberConstant(field), "id",
+                       std::to_string(field->number()));
+      }
+
       stub_h_->Outdent();
       stub_h_->Print("};\n");
     }
@@ -1049,6 +1075,20 @@
       }
       GenerateFieldDescriptor(extension_name, field);
     }
+
+    if (!descriptors.empty()) {
+      stub_h_->Print("enum : int32_t {\n");
+      stub_h_->Indent();
+
+      for (const FieldDescriptor* field : descriptors) {
+        stub_h_->Print("$name$ = $id$,\n", "name",
+                       GetFieldNumberConstant(field), "id",
+                       std::to_string(field->number()));
+      }
+      stub_h_->Outdent();
+      stub_h_->Print("};\n");
+    }
+
     stub_h_->Outdent();
     stub_h_->Print("};\n");
   }
diff --git a/src/trace_processor/containers/BUILD.gn b/src/trace_processor/containers/BUILD.gn
index 43e9603..c724df8 100644
--- a/src/trace_processor/containers/BUILD.gn
+++ b/src/trace_processor/containers/BUILD.gn
@@ -22,6 +22,7 @@
 perfetto_component("containers") {
   public = [
     "bit_vector.h",
+    "implicit_segment_forest.h",
     "null_term_string_view.h",
     "row_map.h",
     "row_map_algorithms.h",
@@ -44,6 +45,7 @@
   testonly = true
   sources = [
     "bit_vector_unittest.cc",
+    "implicit_segment_forest_unittest.cc",
     "null_term_string_view_unittest.cc",
     "row_map_unittest.cc",
     "string_pool_unittest.cc",
diff --git a/src/trace_processor/containers/implicit_segment_forest.h b/src/trace_processor/containers/implicit_segment_forest.h
new file mode 100644
index 0000000..5b58cb4
--- /dev/null
+++ b/src/trace_processor/containers/implicit_segment_forest.h
@@ -0,0 +1,137 @@
+/*
+ * 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_PROCESSOR_CONTAINERS_IMPLICIT_SEGMENT_FOREST_H_
+#define SRC_TRACE_PROCESSOR_CONTAINERS_IMPLICIT_SEGMENT_FOREST_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+
+namespace perfetto::trace_processor {
+
+// An implementation of a segment tree data structure [1] with:
+// 1) parent-child relationships are implicit, saving memory.
+// 2) the requirement for the number of values being a power of two, turning
+//    the tree into a forest.
+//
+// Segment trees are a very powerful data structure allowing O(log(n)) aggregate
+// queries to be performed on an arbitrary range of elements in an array.
+// Specifically, for `T x[n]`, and an associative and commutative operation
+// AggOp (e.g. +, *, min, max, etc.), segment trees can compute
+// ```
+//   T y = AggOp()(x[i], x[i + 1], x[i + 2], ..., x[j])
+// ```
+// in O(log(n)) time.
+//
+// Practically, in trace processor, this is useful for computing aggregations
+// over events in a trace. For example:
+// ```
+// struct Slice { int64_t ts; int64_t dur; };
+// struct MaxDurSlice {
+//   Slice operator()(const Slice& a, const Slice& b) {
+//     return a.dur < b.dur ? b : a;
+//   }
+// }
+// using MipMap = ImplicitSegmentForest<Slice, MaxDurSlice>;
+// ```
+// allows building a "mipmap" [2] of a track in a trace in a UI. The UI can show
+// a representation of the items in the track when very zoomed out while
+// skipping the rendering slices which are smaller than one pixel.
+//
+// The design and implementation of this class takes heavy inspiration from
+// Tristan Hume's "IForestIndex" data structure [3] as described in his blog
+// post [4].
+//
+// [1] https://en.algorithmica.org/hpc/data-structures/segment-trees/
+// [2] https://en.wikipedia.org/wiki/Mipmap
+// [3]
+// https://github.com/trishume/gigatrace/blob/dfde0d7244f356bdc9aeefb387d904dd8b09d94a/src/iforest.rs
+// [4] https://thume.ca/2021/03/14/iforests/
+template <typename T, typename AggOp>
+class ImplicitSegmentForest {
+ public:
+  // Computes the aggregation (as specified by operator() in AggOp) over all
+  // elements in the tree between the indices [start, end). Requires that
+  // start < end.
+  //
+  // Complexity:
+  // This function performs O(log(n)) operations (n = end - start).
+  //
+  // Returns:
+  //  1) values[start]: if start + 1 == end
+  //  2) AggOp()(values[start], ..., values[end - 1]) otherwise
+  T Query(uint32_t start, uint32_t end) const {
+    PERFETTO_DCHECK(start < end);
+
+    const uint32_t in_start = start * 2;
+    const uint32_t in_end = end * 2;
+
+    uint32_t first_skip = LargestPrefixInsideSkip(in_start, in_end);
+    T aggregated = values_[AggNode(in_start, first_skip)];
+    for (uint32_t i = in_start + first_skip; i < in_end;) {
+      uint32_t skip = LargestPrefixInsideSkip(i, in_end);
+      aggregated = AggOp()(aggregated, values_[AggNode(i, skip)]);
+      i += skip;
+    }
+    return aggregated;
+  }
+
+  // Pushes a new element to right-most part of the tree. This index of this
+  // element can be used in future calls to |Query|.
+  void Push(T v) {
+    values_.emplace_back(std::move(v));
+
+    size_t len = values_.size();
+    auto levels_to_index = static_cast<uint32_t>(__builtin_ctzl(~len)) - 1;
+
+    size_t cur = len - 1;
+    for (uint32_t level = 0; level < levels_to_index; ++level) {
+      size_t prev_higher_level = cur - (1 << level);
+      values_[prev_higher_level] =
+          AggOp()(values_[prev_higher_level], values_[cur]);
+      cur = prev_higher_level;
+    }
+    values_.emplace_back(values_[len - (1 << levels_to_index)]);
+  }
+
+  // Returns the value at |n| in the tree: this corresponds to the |n|th
+  // element |Push|-ed into the tree.
+  const T& operator[](uint32_t n) { return values_[n * 2]; }
+
+  // Returns the number of elements pushed into the forest.
+  uint32_t size() const { return static_cast<uint32_t>(values_.size() / 2); }
+
+ private:
+  static uint32_t Lsp(uint32_t x) { return x & -x; }
+  static uint32_t Msp(uint32_t x) {
+    return (1u << (sizeof(x) * 8 - 1)) >> __builtin_clz(x);
+  }
+  static uint32_t LargestPrefixInsideSkip(uint32_t min, uint32_t max) {
+    return Lsp(min | Msp(max - min));
+  }
+  static uint32_t AggNode(uint32_t i, uint32_t offset) {
+    return i + (offset >> 1) - 1;
+  }
+
+  std::vector<T> values_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_CONTAINERS_IMPLICIT_SEGMENT_FOREST_H_
diff --git a/src/trace_processor/containers/implicit_segment_forest_unittest.cc b/src/trace_processor/containers/implicit_segment_forest_unittest.cc
new file mode 100644
index 0000000..16dd262
--- /dev/null
+++ b/src/trace_processor/containers/implicit_segment_forest_unittest.cc
@@ -0,0 +1,76 @@
+/*
+ * 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_processor/containers/implicit_segment_forest.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <numeric>
+#include <random>
+#include <vector>
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+struct Value {
+  uint32_t value;
+};
+
+struct Sum {
+  Value operator()(const Value& a, const Value& b) {
+    return Value{a.value + b.value};
+  }
+};
+
+TEST(ImplicitSegmentTree, SimpleSum) {
+  std::vector<uint32_t> res = {209, 330, 901, 3, 10, 0, 3903, 309, 490};
+
+  ImplicitSegmentForest<Value, Sum> forest;
+  for (uint32_t x : res) {
+    forest.Push(Value{x});
+  }
+
+  for (uint32_t i = 0; i < res.size(); ++i) {
+    for (uint32_t j = i + 1; j < res.size(); ++j) {
+      ASSERT_EQ(forest.Query(i, j).value,
+                std::accumulate(res.begin() + i, res.begin() + j, 0u));
+    }
+  }
+}
+
+TEST(ImplicitSegmentTree, Stress) {
+  static constexpr size_t kCount = 9249;
+  std::minstd_rand0 rng(42);
+
+  std::vector<uint32_t> res;
+  ImplicitSegmentForest<Value, Sum> forest;
+  for (uint32_t i = 0; i < kCount; ++i) {
+    res.push_back(static_cast<uint32_t>(rng()));
+    forest.Push(Value{res.back()});
+  }
+
+  for (uint32_t i = 0; i < 10000; ++i) {
+    uint32_t s = rng() % kCount;
+    uint32_t e = s + 1 + (rng() % (kCount - s));
+    ASSERT_EQ(forest.Query(s, e).value,
+              std::accumulate(res.begin() + s, res.begin() + e, 0u));
+  }
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/db/column/BUILD.gn b/src/trace_processor/db/column/BUILD.gn
index 74d1611..d53e7ae 100644
--- a/src/trace_processor/db/column/BUILD.gn
+++ b/src/trace_processor/db/column/BUILD.gn
@@ -75,6 +75,7 @@
   sources = [
     "arrangement_overlay_unittest.cc",
     "dense_null_overlay_unittest.cc",
+    "fake_storage_unittest.cc",
     "id_storage_unittest.cc",
     "null_overlay_unittest.cc",
     "numeric_storage_unittest.cc",
diff --git a/src/trace_processor/db/column/dense_null_overlay.cc b/src/trace_processor/db/column/dense_null_overlay.cc
index e171ec0..55003fa 100644
--- a/src/trace_processor/db/column/dense_null_overlay.cc
+++ b/src/trace_processor/db/column/dense_null_overlay.cc
@@ -82,10 +82,10 @@
       case SearchValidationResult::kNoData: {
         // There is no need to search in underlying storage. It's enough to
         // intersect the |non_null_|.
-        BitVector res = non_null_->IntersectRange(in.start, in.end);
-        res.Not();
+        BitVector res = non_null_->Copy();
         res.Resize(in.end, false);
-        return RangeOrBitVector(std::move(res));
+        res.Not();
+        return RangeOrBitVector(res.IntersectRange(in.start, in.end));
       }
       case SearchValidationResult::kAllData:
         return RangeOrBitVector(in);
diff --git a/src/trace_processor/db/column/fake_storage.cc b/src/trace_processor/db/column/fake_storage.cc
index f587c77..babfb7c 100644
--- a/src/trace_processor/db/column/fake_storage.cc
+++ b/src/trace_processor/db/column/fake_storage.cc
@@ -41,6 +41,7 @@
 SingleSearchResult FakeStorageChain::SingleSearch(FilterOp,
                                                   SqlValue,
                                                   uint32_t i) const {
+  PERFETTO_CHECK(i < size_);
   switch (strategy_) {
     case kAll:
       return SingleSearchResult::kMatch;
@@ -115,37 +116,37 @@
     FilterOp,
     SqlValue,
     const OrderedIndices& indices) const {
-  if (strategy_ == kAll) {
-    return {0, indices.size};
-  }
-
-  if (strategy_ == kNone) {
-    return {};
-  }
-
-  if (strategy_ == kRange) {
-    // We are looking at intersection of |range_| and |indices_|.
-    const uint32_t* first_in_range = std::partition_point(
-        indices.data, indices.data + indices.size,
-        [this](uint32_t i) { return !range_.Contains(i); });
-    const uint32_t* first_outside_range =
-        std::partition_point(first_in_range, indices.data + indices.size,
-                             [this](uint32_t i) { return range_.Contains(i); });
-    return {static_cast<uint32_t>(std::distance(indices.data, first_in_range)),
-            static_cast<uint32_t>(
-                std::distance(indices.data, first_outside_range))};
-  }
-
-  PERFETTO_DCHECK(strategy_ == kBitVector);
-  // We are looking at intersection of |range_| and |bit_vector_|.
-  const uint32_t* first_set = std::partition_point(
-      indices.data, indices.data + indices.size,
-      [this](uint32_t i) { return !bit_vector_.IsSet(i); });
-  const uint32_t* first_non_set =
-      std::partition_point(first_set, indices.data + indices.size,
-                           [this](uint32_t i) { return bit_vector_.IsSet(i); });
-  return {static_cast<uint32_t>(std::distance(indices.data, first_set)),
+  switch (strategy_) {
+    case kAll:
+      return {0, indices.size};
+    case kNone:
+      return {};
+    case kRange: {
+      // We are looking at intersection of |range_| and |indices_|.
+      const uint32_t* first_in_range = std::partition_point(
+          indices.data, indices.data + indices.size,
+          [this](uint32_t i) { return !range_.Contains(i); });
+      const uint32_t* first_outside_range = std::partition_point(
+          first_in_range, indices.data + indices.size,
+          [this](uint32_t i) { return range_.Contains(i); });
+      return {
+          static_cast<uint32_t>(std::distance(indices.data, first_in_range)),
+          static_cast<uint32_t>(
+              std::distance(indices.data, first_outside_range))};
+    }
+    case kBitVector:
+      // We are looking at intersection of |range_| and |bit_vector_|.
+      const uint32_t* first_set = std::partition_point(
+          indices.data, indices.data + indices.size,
+          [this](uint32_t i) { return !bit_vector_.IsSet(i); });
+      const uint32_t* first_non_set = std::partition_point(
+          first_set, indices.data + indices.size,
+          [this](uint32_t i) { return bit_vector_.IsSet(i); });
+      return {
+          static_cast<uint32_t>(std::distance(indices.data, first_set)),
           static_cast<uint32_t>(std::distance(indices.data, first_non_set))};
+  }
+  PERFETTO_FATAL("For GCC");
 }
 
 void FakeStorageChain::StableSort(SortToken*, SortToken*, SortDirection) const {
diff --git a/src/trace_processor/db/column/fake_storage_unittest.cc b/src/trace_processor/db/column/fake_storage_unittest.cc
new file mode 100644
index 0000000..0bc0211
--- /dev/null
+++ b/src/trace_processor/db/column/fake_storage_unittest.cc
@@ -0,0 +1,210 @@
+/*
+ * 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_processor/db/column/fake_storage.h"
+
+#include <cstdint>
+#include <limits>
+#include <vector>
+
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/column/data_layer.h"
+#include "src/trace_processor/db/column/types.h"
+#include "src/trace_processor/db/column/utils.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+
+inline bool operator==(const Range& a, const Range& b) {
+  return std::tie(a.start, a.end) == std::tie(b.start, b.end);
+}
+
+inline bool operator==(const BitVector& a, const BitVector& b) {
+  return a.size() == b.size() && a.CountSetBits() == b.CountSetBits();
+}
+
+namespace column {
+namespace {
+
+using testing::ElementsAre;
+using testing::IsEmpty;
+
+using Indices = DataLayerChain::Indices;
+using OrderedIndices = DataLayerChain::OrderedIndices;
+
+TEST(FakeStorage, ValidateSearchConstraints) {
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(10);
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(10);
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3, 4, 5});
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+}
+
+TEST(FakeStorage, SingleSearch) {
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(10);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 5u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(10);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 5u),
+              SingleSearchResult::kNoMatch);
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3, 4, 5});
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0u),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+}
+
+TEST(FakeStorage, IndexSearchValidated) {
+  {
+    // All passes
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchAll(5);
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 1, 2));
+  }
+  {
+    // None passes
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchNone(5);
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_TRUE(utils::ExtractPayloadForTesting(indices).empty());
+  }
+  {
+    // BitVector
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+  {
+    // Index vector
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3});
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+  {
+    // Range
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+}
+
+TEST(FakeStorage, OrderedIndexSearchValidated) {
+  std::vector<uint32_t> table_idx{4, 3, 2, 1};
+  OrderedIndices indices{table_idx.data(), uint32_t(table_idx.size()),
+                         Indices::State::kNonmonotonic};
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(5);
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 4));
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(5);
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 0));
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 0, 1, 1, 1});
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 3));
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3});
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(1, 4));
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(1, 4));
+  }
+}
+
+}  // namespace
+}  // namespace column
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/db/column/null_overlay.cc b/src/trace_processor/db/column/null_overlay.cc
index fa1e679..b1f8f45 100644
--- a/src/trace_processor/db/column/null_overlay.cc
+++ b/src/trace_processor/db/column/null_overlay.cc
@@ -129,10 +129,10 @@
       case SearchValidationResult::kNoData: {
         // There is no need to search in underlying storage. It's enough to
         // intersect the |non_null_|.
-        BitVector res = non_null_->IntersectRange(in.start, in.end);
-        res.Not();
+        BitVector res = non_null_->Copy();
         res.Resize(in.end, false);
-        return RangeOrBitVector(std::move(res));
+        res.Not();
+        return RangeOrBitVector(res.IntersectRange(in.start, in.end));
       }
       case SearchValidationResult::kAllData:
         return RangeOrBitVector(in);
diff --git a/src/trace_processor/db/column/numeric_storage.h b/src/trace_processor/db/column/numeric_storage.h
index f7cb51c..821fd40 100644
--- a/src/trace_processor/db/column/numeric_storage.h
+++ b/src/trace_processor/db/column/numeric_storage.h
@@ -100,21 +100,7 @@
     SingleSearchResult SingleSearch(FilterOp op,
                                     SqlValue sql_val,
                                     uint32_t i) const override {
-      if constexpr (std::is_same_v<T, double>) {
-        if (sql_val.type != SqlValue::kDouble) {
-          return SingleSearchResult::kNeedsFullSearch;
-        }
-        return utils::SingleSearchNumeric(op, (*vector_)[i],
-                                          sql_val.double_value);
-      } else {
-        if (sql_val.type != SqlValue::kLong ||
-            sql_val.long_value > std::numeric_limits<T>::max() ||
-            sql_val.long_value < std::numeric_limits<T>::min()) {
-          return SingleSearchResult::kNeedsFullSearch;
-        }
-        return utils::SingleSearchNumeric(op, (*vector_)[i],
-                                          static_cast<T>(sql_val.long_value));
-      }
+      return utils::SingleSearchNumeric(op, (*vector_)[i], sql_val);
     }
 
     void StableSort(SortToken* start,
diff --git a/src/trace_processor/db/column/numeric_storage_unittest.cc b/src/trace_processor/db/column/numeric_storage_unittest.cc
index 2b20306..46ef920 100644
--- a/src/trace_processor/db/column/numeric_storage_unittest.cc
+++ b/src/trace_processor/db/column/numeric_storage_unittest.cc
@@ -187,6 +187,11 @@
             SingleSearchResult::kMatch);
   ASSERT_EQ(chain->SingleSearch(FilterOp::kGe, SqlValue::Long(0), 5),
             SingleSearchResult::kNoMatch);
+
+  ASSERT_EQ(chain->SingleSearch(FilterOp::kIsNull, SqlValue(), 0),
+            SingleSearchResult::kNoMatch);
+  ASSERT_EQ(chain->SingleSearch(FilterOp::kIsNotNull, SqlValue(), 0),
+            SingleSearchResult::kMatch);
 }
 
 TEST(NumericStorage, Search) {
diff --git a/src/trace_processor/db/column/range_overlay_unittest.cc b/src/trace_processor/db/column/range_overlay_unittest.cc
index a965be3..240e998 100644
--- a/src/trace_processor/db/column/range_overlay_unittest.cc
+++ b/src/trace_processor/db/column/range_overlay_unittest.cc
@@ -95,14 +95,17 @@
 TEST(RangeOverlay, IndexSearch) {
   auto fake =
       FakeStorageChain::SearchSubset(8, BitVector({0, 1, 0, 1, 0, 1, 0, 0}));
+
+  // {true, false}
   Range range(3, 5);
   RangeOverlay storage(&range);
   auto chain = storage.MakeChain(std::move(fake));
 
+  // {true, false, true}
   Indices indices = Indices::CreateWithIndexPayloadForTesting(
-      {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+      {0, 1, 0}, Indices::State::kNonmonotonic);
   chain->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
-  ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(1u));
+  ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
 }
 
 TEST(RangeOverlay, StableSort) {
diff --git a/src/trace_processor/db/column/set_id_storage.cc b/src/trace_processor/db/column/set_id_storage.cc
index 6e4719f..ae1c167 100644
--- a/src/trace_processor/db/column/set_id_storage.cc
+++ b/src/trace_processor/db/column/set_id_storage.cc
@@ -72,15 +72,7 @@
 SingleSearchResult SetIdStorage::ChainImpl::SingleSearch(FilterOp op,
                                                          SqlValue sql_val,
                                                          uint32_t i) const {
-  if (sql_val.type != SqlValue::kLong ||
-      sql_val.long_value > std::numeric_limits<uint32_t>::max() ||
-      sql_val.long_value < std::numeric_limits<uint32_t>::min()) {
-    // Because of the large amount of code needing for handling comparisions
-    // with doubles or out of range values, just defer to the full search.
-    return SingleSearchResult::kNeedsFullSearch;
-  }
-  return utils::SingleSearchNumeric(op, (*values_)[i],
-                                    static_cast<uint32_t>(sql_val.long_value));
+  return utils::SingleSearchNumeric(op, (*values_)[i], sql_val);
 }
 
 SearchValidationResult SetIdStorage::ChainImpl::ValidateSearchConstraints(
diff --git a/src/trace_processor/db/column/types.h b/src/trace_processor/db/column/types.h
index b59084b..6ec11fa 100644
--- a/src/trace_processor/db/column/types.h
+++ b/src/trace_processor/db/column/types.h
@@ -96,6 +96,15 @@
   bool desc;
 };
 
+// Structured data used to determine what Trace Processor will query using
+// CEngine.
+struct Query {
+  // Query constraints.
+  std::vector<Constraint> constraints;
+  // Query order bys.
+  std::vector<Order> orders;
+};
+
 // The enum type of the column.
 // Public only to stop GCC complaining about templates being defined in a
 // non-namespace scope (see ColumnTypeHelper below).
diff --git a/src/trace_processor/db/column/utils.h b/src/trace_processor/db/column/utils.h
index 14c6456..9533d8cd 100644
--- a/src/trace_processor/db/column/utils.h
+++ b/src/trace_processor/db/column/utils.h
@@ -19,7 +19,9 @@
 #include <algorithm>
 #include <cstdint>
 #include <functional>
+#include <limits>
 #include <optional>
+#include <type_traits>
 #include <vector>
 
 #include "perfetto/base/logging.h"
@@ -29,6 +31,36 @@
 #include "src/trace_processor/db/column/types.h"
 
 namespace perfetto::trace_processor::column::utils {
+namespace internal {
+
+template <typename T, typename Comparator>
+SingleSearchResult SingleSearchNumeric(T left, const SqlValue& right_v) {
+  if constexpr (std::is_same_v<T, double>) {
+    if (right_v.type != SqlValue::kDouble) {
+      // Because of the large amount of code needing for handling comparisons
+      // with integers, just defer to the full search.
+      return SingleSearchResult::kNeedsFullSearch;
+    }
+    return Comparator()(left, right_v.double_value)
+               ? SingleSearchResult::kMatch
+               : SingleSearchResult::kNoMatch;
+  } else if constexpr (std::is_integral_v<T>) {
+    if (right_v.type != SqlValue::kLong ||
+        right_v.long_value > std::numeric_limits<T>::max() ||
+        right_v.long_value < std::numeric_limits<T>::min()) {
+      // Because of the large amount of code needing for handling comparisons
+      // with doubles or out of range values, just defer to the full search.
+      return SingleSearchResult::kNeedsFullSearch;
+    }
+    return Comparator()(left, static_cast<T>(right_v.long_value))
+               ? SingleSearchResult::kMatch
+               : SingleSearchResult::kNoMatch;
+  } else {
+    static_assert(std::is_same_v<T, void>, "Illegal type");
+  }
+}
+
+}  // namespace internal
 
 template <typename Comparator, typename ValType, typename DataType>
 void LinearSearchWithComparator(ValType val,
@@ -77,27 +109,25 @@
 }
 
 template <typename T>
-SingleSearchResult SingleSearchNumeric(FilterOp op, T left, T right) {
+SingleSearchResult SingleSearchNumeric(FilterOp op,
+                                       T left,
+                                       const SqlValue& right_v) {
   switch (op) {
     case FilterOp::kEq:
-      return std::equal_to<T>()(left, right) ? SingleSearchResult::kMatch
-                                             : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::equal_to<T>>(left, right_v);
     case FilterOp::kNe:
-      return std::not_equal_to<T>()(left, right) ? SingleSearchResult::kMatch
-                                                 : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::not_equal_to<T>>(left,
+                                                                    right_v);
     case FilterOp::kGe:
-      return std::greater_equal<T>()(left, right)
-                 ? SingleSearchResult::kMatch
-                 : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::greater_equal<T>>(left,
+                                                                     right_v);
     case FilterOp::kGt:
-      return std::greater<T>()(left, right) ? SingleSearchResult::kMatch
-                                            : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::greater<T>>(left, right_v);
     case FilterOp::kLe:
-      return std::less_equal<T>()(left, right) ? SingleSearchResult::kMatch
-                                               : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::less_equal<T>>(left,
+                                                                  right_v);
     case FilterOp::kLt:
-      return std::less<T>()(left, right) ? SingleSearchResult::kMatch
-                                         : SingleSearchResult::kNoMatch;
+      return internal::SingleSearchNumeric<T, std::less<T>>(left, right_v);
     case FilterOp::kIsNotNull:
       return SingleSearchResult::kMatch;
     case FilterOp::kGlob:
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 8502e04..35f743a 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -234,15 +234,17 @@
 void BenchmarkSliceTableFilter(benchmark::State& state,
                                SliceTableForBenchmark& table,
                                std::initializer_list<Constraint> c) {
+  Query q;
+  q.constraints = c;
   for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.QueryToRowMap(c, {}));
+    benchmark::DoNotOptimize(table.table_.QueryToRowMap(q));
   }
   state.counters["s/row"] =
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap(c, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -263,15 +265,17 @@
     benchmark::State& state,
     ExpectedFrameTimelineTableForBenchmark& table,
     Constraint c) {
+  Query q;
+  q.constraints = {c};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.QueryToRowMap({c}, {}));
+    benchmark::DoNotOptimize(table.table_.QueryToRowMap(q));
   }
   state.counters["s/row"] =
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -279,15 +283,17 @@
 void BenchmarkFtraceEventTableFilter(benchmark::State& state,
                                      FtraceEventTableForBenchmark& table,
                                      std::initializer_list<Constraint> c) {
+  Query q;
+  q.constraints = c;
   for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.QueryToRowMap(c, {}));
+    benchmark::DoNotOptimize(table.table_.QueryToRowMap(q));
   }
   state.counters["s/row"] =
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -424,15 +430,17 @@
 
   Constraint c{table.table_.track_id().index_in_table(), FilterOp::kGt,
                SqlValue::Long(10)};
+  Query q;
+  q.constraints = {c};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(slice_sorted_with_duration.QueryToRowMap({c}, {}));
+    benchmark::DoNotOptimize(slice_sorted_with_duration.QueryToRowMap(q));
   }
   state.counters["s/row"] = benchmark::Counter(
       static_cast<double>(slice_sorted_with_duration.row_count()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -443,15 +451,17 @@
   HeapGraphObjectTableForBenchmark table(state);
   Constraint c{table.table_.reference_set_id().index_in_table(), FilterOp::kGt,
                SqlValue::Long(1000)};
+  Query q;
+  q.constraints = {c};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.QueryToRowMap({c}, {}));
+    benchmark::DoNotOptimize(table.table_.QueryToRowMap(q));
   }
   state.counters["s/row"] =
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -461,15 +471,17 @@
   HeapGraphObjectTableForBenchmark table(state);
   Constraint c{table.table_.reference_set_id().index_in_table(),
                FilterOp::kIsNull, SqlValue()};
+  Query q;
+  q.constraints = {c};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.QueryToRowMap({c}, {}));
+    benchmark::DoNotOptimize(table.table_.QueryToRowMap(q));
   }
   state.counters["s/row"] =
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
@@ -500,15 +512,17 @@
 
   Constraint c{table.table_.dur().index_in_table(), FilterOp::kGt,
                SqlValue::Long(10)};
+  Query q;
+  q.constraints = {c};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(slice_sorted_with_duration.QueryToRowMap({c}, {}));
+    benchmark::DoNotOptimize(slice_sorted_with_duration.QueryToRowMap(q));
   }
   state.counters["s/row"] = benchmark::Counter(
       static_cast<double>(slice_sorted_with_duration.row_count()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
   state.counters["s/out"] = benchmark::Counter(
-      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      static_cast<double>(table.table_.QueryToRowMap(q).size()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
 }
diff --git a/src/trace_processor/db/table.cc b/src/trace_processor/db/table.cc
index 641c745..0505bf4 100644
--- a/src/trace_processor/db/table.cc
+++ b/src/trace_processor/db/table.cc
@@ -87,8 +87,9 @@
   return {string_pool_, row_count_, std::move(cols), {}};
 }
 
-RowMap Table::QueryToRowMap(const std::vector<Constraint>& cs,
-                            const std::vector<Order>& ob) const {
+RowMap Table::QueryToRowMap(const Query& q) const {
+  const auto& cs = q.constraints;
+  const auto& ob = q.orders;
   // We need to delay creation of the chains to this point because of Chrome
   // does not want the binary size overhead of including the chain
   // implementations. As they also don't query tables (instead just iterating)
@@ -134,7 +135,9 @@
   // Return a copy of this table with the RowMaps using the computed ordered
   // RowMap.
   Table table = CopyExceptOverlays();
-  RowMap rm = QueryToRowMap({}, ob);
+  Query q;
+  q.orders = ob;
+  RowMap rm = QueryToRowMap(q);
   for (const ColumnStorageOverlay& overlay : overlays_) {
     table.overlays_.emplace_back(overlay.SelectRows(rm));
     PERFETTO_DCHECK(table.overlays_.back().size() == table.row_count());
diff --git a/src/trace_processor/db/table.h b/src/trace_processor/db/table.h
index 7d53106..655fa73 100644
--- a/src/trace_processor/db/table.h
+++ b/src/trace_processor/db/table.h
@@ -130,8 +130,7 @@
 
   // Filters and sorts the tables with the arguments specified, returning the
   // result as a RowMap.
-  RowMap QueryToRowMap(const std::vector<Constraint>&,
-                       const std::vector<Order>&) const;
+  RowMap QueryToRowMap(const Query&) const;
 
   // Applies the RowMap |rm| onto this table and returns an iterator over the
   // resulting rows.
diff --git a/src/trace_processor/importers/common/process_tracker.cc b/src/trace_processor/importers/common/process_tracker.cc
index e1d23a3..f034867 100644
--- a/src/trace_processor/importers/common/process_tracker.cc
+++ b/src/trace_processor/importers/common/process_tracker.cc
@@ -15,11 +15,11 @@
  */
 
 #include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/storage/stats.h"
 
-#include <cinttypes>
 #include <utility>
 
+#include "src/trace_processor/storage/stats.h"
+
 namespace perfetto {
 namespace trace_processor {
 
@@ -102,7 +102,7 @@
   // Remove the thread from the list of threads being tracked as any event after
   // this one should be ignored.
   auto& vector = tids_[tid];
-  vector.erase(std::remove(vector.begin(), vector.end(), utid));
+  vector.erase(std::remove(vector.begin(), vector.end(), utid), vector.end());
 
   auto opt_upid = thread_table->upid()[utid];
   if (!opt_upid.has_value() || process_table->pid()[*opt_upid] != tid)
@@ -536,13 +536,11 @@
 }
 
 void ProcessTracker::SetPidZeroIsUpidZeroIdleProcess() {
-  auto swapper_id = context_->storage->InternString("swapper");
-
   // Create a mapping from (t|p)id 0 -> u(t|p)id for the idle process.
   tids_.Insert(0, std::vector<UniqueTid>{swapper_utid_});
   pids_.Insert(0, swapper_upid_);
 
-  // Set the hardcoded, constant "swapper" thread name to the thread.
+  auto swapper_id = context_->storage->InternString("swapper");
   UpdateThreadName(0, swapper_id, ThreadNamePriority::kTraceProcessorConstant);
 }
 
diff --git a/src/trace_processor/importers/common/process_tracker.h b/src/trace_processor/importers/common/process_tracker.h
index 3808f99..e5ccd39 100644
--- a/src/trace_processor/importers/common/process_tracker.h
+++ b/src/trace_processor/importers/common/process_tracker.h
@@ -17,8 +17,11 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PROCESS_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PROCESS_TRACKER_H_
 
-#include <tuple>
+#include <stdint.h>
+
+#include <optional>
 #include <unordered_set>
+#include <vector>
 
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_view.h"
@@ -35,6 +38,7 @@
 enum class ThreadNamePriority {
   kOther = 0,
   kFtrace = 1,
+  kEtwTrace = 1,
   kProcessTree = 2,
   kTrackDescriptorThreadType = 3,
   kTrackDescriptor = 4,
diff --git a/src/trace_processor/importers/common/thread_state_tracker.cc b/src/trace_processor/importers/common/thread_state_tracker.cc
index 37d26d1..2d98ffb 100644
--- a/src/trace_processor/importers/common/thread_state_tracker.cc
+++ b/src/trace_processor/importers/common/thread_state_tracker.cc
@@ -15,8 +15,10 @@
  */
 
 #include "src/trace_processor/importers/common/thread_state_tracker.h"
+
 #include <cstdint>
 #include <optional>
+
 #include "src/trace_processor/importers/common/process_tracker.h"
 
 namespace perfetto {
@@ -182,7 +184,10 @@
   // If common_flags contains TRACE_FLAG_HARDIRQ | TRACE_FLAG_SOFTIRQ, wakeup
   // was emitted in interrupt context.
   // See:
-  // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/include/trace/trace_events.h
+  // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/include/linux/trace_events.h
+  // TODO(rsavitski): we could also include TRACE_FLAG_NMI for a complete
+  // "interrupt context" meaning. But at the moment it's not necessary as this
+  // is used for sched_waking events, which are not emitted from NMI contexts.
   return common_flags & (0x08 | 0x10) ? 1 : 0;
 }
 
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index 6c586e0..608aca3 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -51,6 +51,9 @@
 void TraceParser::ParseInlineSchedWaking(uint32_t, int64_t, InlineSchedWaking) {
   PERFETTO_FATAL("Wrong parser type");
 }
+void TraceParser::ParseEtwEvent(uint32_t, int64_t, TracePacketData) {
+  PERFETTO_FATAL("Wrong parser type");
+}
 
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index d948d27..9c7098f 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -43,6 +43,7 @@
   virtual void ParseTrackEvent(int64_t, TrackEventData);
   virtual void ParseSystraceLine(int64_t, SystraceLine);
 
+  virtual void ParseEtwEvent(uint32_t, int64_t, TracePacketData);
   virtual void ParseFtraceEvent(uint32_t, int64_t, TracePacketData);
   virtual void ParseInlineSchedSwitch(uint32_t, int64_t, InlineSchedSwitch);
   virtual void ParseInlineSchedWaking(uint32_t, int64_t, InlineSchedWaking);
diff --git a/src/trace_processor/importers/etw/BUILD.gn b/src/trace_processor/importers/etw/BUILD.gn
index d8985b1..23d8965 100644
--- a/src/trace_processor/importers/etw/BUILD.gn
+++ b/src/trace_processor/importers/etw/BUILD.gn
@@ -14,14 +14,30 @@
 
 import("../../../../gn/test.gni")
 
-source_set("full") {
+source_set("minimal") {
   sources = [
     "etw_module.cc",
     "etw_module.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../common:parser_types",
+    "../common:trace_parser_hdr",
+    "../proto:proto_importer_module",
+  ]
+}
+
+source_set("full") {
+  sources = [
+    "etw_module_impl.cc",
+    "etw_module_impl.h",
+    "etw_parser.cc",
+    "etw_parser.h",
     "etw_tokenizer.cc",
     "etw_tokenizer.h",
   ]
   deps = [
+    ":minimal",
     "../../../../gn:default_deps",
     "../../../../protos/perfetto/common:zero",
     "../../../../protos/perfetto/trace:zero",
diff --git a/src/trace_processor/importers/etw/etw_module.cc b/src/trace_processor/importers/etw/etw_module.cc
index 508d2ca..a1a9d3f 100644
--- a/src/trace_processor/importers/etw/etw_module.cc
+++ b/src/trace_processor/importers/etw/etw_module.cc
@@ -15,37 +15,15 @@
  */
 
 #include "src/trace_processor/importers/etw/etw_module.h"
-#include "perfetto/base/build_config.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/etw/etw_tokenizer.h"
 
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 
 namespace perfetto {
 namespace trace_processor {
 
-using perfetto::protos::pbzero::TracePacket;
-
-EtwModule::EtwModule(TraceProcessorContext* context) : tokenizer_(context) {
-  RegisterForField(TracePacket::kEtwEventsFieldNumber, context);
-}
-
-ModuleResult EtwModule::TokenizePacket(
-    const protos::pbzero::TracePacket::Decoder& decoder,
-    TraceBlobView* packet,
-    int64_t /*packet_timestamp*/,
-    PacketSequenceState* seq_state,
-    uint32_t field_id) {
-  switch (field_id) {
-    case TracePacket::kEtwEventsFieldNumber: {
-      auto etw_field = decoder.etw_events();
-      tokenizer_.TokenizeEtwBundle(
-          packet->slice(etw_field.data, etw_field.size), seq_state);
-      return ModuleResult::Handled();
-    }
-  }
-  return ModuleResult::Ignored();
-}
+void EtwModule::ParseEtwEventData(uint32_t /*cpu*/,
+                                  int64_t /*ts*/,
+                                  const TracePacketData&) {}
 
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/etw/etw_module.h b/src/trace_processor/importers/etw/etw_module.h
index 1624af1..5f80271 100644
--- a/src/trace_processor/importers/etw/etw_module.h
+++ b/src/trace_processor/importers/etw/etw_module.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -17,11 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_MODULE_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_MODULE_H_
 
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "src/trace_processor/importers/common/parser_types.h"
-#include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/etw/etw_module.h"
-#include "src/trace_processor/importers/etw/etw_tokenizer.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 
 namespace perfetto {
@@ -29,17 +25,9 @@
 
 class EtwModule : public ProtoImporterModule {
  public:
-  explicit EtwModule(TraceProcessorContext* context);
-
-  ModuleResult TokenizePacket(
-      const protos::pbzero::TracePacket::Decoder& decoder,
-      TraceBlobView* packet,
-      int64_t packet_timestamp,
-      PacketSequenceState* state,
-      uint32_t field_id) override;
-
- private:
-  EtwTokenizer tokenizer_;
+  virtual void ParseEtwEventData(uint32_t cpu,
+                                 int64_t ts,
+                                 const TracePacketData& data);
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/etw/etw_module_impl.cc b/src/trace_processor/importers/etw/etw_module_impl.cc
new file mode 100644
index 0000000..7c44d04
--- /dev/null
+++ b/src/trace_processor/importers/etw/etw_module_impl.cc
@@ -0,0 +1,52 @@
+/*
+ * 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_processor/importers/etw/etw_module_impl.h"
+
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/etw/etw_tokenizer.h"
+
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+using perfetto::protos::pbzero::TracePacket;
+
+EtwModuleImpl::EtwModuleImpl(TraceProcessorContext* context)
+    : tokenizer_(context), parser_(context) {
+  RegisterForField(TracePacket::kEtwEventsFieldNumber, context);
+}
+
+ModuleResult EtwModuleImpl::TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* packet,
+    int64_t /*packet_timestamp*/,
+    PacketSequenceState* seq_state,
+    uint32_t field_id) {
+  switch (field_id) {
+    case TracePacket::kEtwEventsFieldNumber: {
+      auto etw_field = decoder.etw_events();
+      tokenizer_.TokenizeEtwBundle(
+          packet->slice(etw_field.data, etw_field.size), seq_state);
+      return ModuleResult::Handled();
+    }
+  }
+  return ModuleResult::Ignored();
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/etw/etw_module_impl.h b/src/trace_processor/importers/etw/etw_module_impl.h
new file mode 100644
index 0000000..af05fc8
--- /dev/null
+++ b/src/trace_processor/importers/etw/etw_module_impl.h
@@ -0,0 +1,61 @@
+/*
+ * 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_PROCESSOR_IMPORTERS_ETW_ETW_MODULE_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_MODULE_IMPL_H_
+
+#include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/etw/etw_module.h"
+#include "src/trace_processor/importers/etw/etw_parser.h"
+#include "src/trace_processor/importers/etw/etw_tokenizer.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
+
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceBlobView;
+
+class EtwModuleImpl : public EtwModule {
+ public:
+  explicit EtwModuleImpl(TraceProcessorContext* context);
+
+  ModuleResult TokenizePacket(
+      const protos::pbzero::TracePacket::Decoder& decoder,
+      TraceBlobView* packet,
+      int64_t packet_timestamp,
+      PacketSequenceState* state,
+      uint32_t field_id) override;
+
+  void ParseEtwEventData(uint32_t cpu,
+                         int64_t ts,
+                         const TracePacketData& data) override {
+    util::Status res = parser_.ParseEtwEvent(cpu, ts, data);
+    if (!res.ok()) {
+      PERFETTO_ELOG("%s", res.message().c_str());
+    }
+  }
+
+ private:
+  EtwTokenizer tokenizer_;
+  EtwParser parser_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_MODULE_IMPL_H_
diff --git a/src/trace_processor/importers/etw/etw_parser.cc b/src/trace_processor/importers/etw/etw_parser.cc
new file mode 100644
index 0000000..99f790d
--- /dev/null
+++ b/src/trace_processor/importers/etw/etw_parser.cc
@@ -0,0 +1,140 @@
+/*
+ etw* 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_processor/importers/etw/etw_parser.h"
+
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/sched_event_tracker.h"
+#include "src/trace_processor/importers/common/thread_state_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+#include "protos/perfetto/trace/etw/etw.pbzero.h"
+#include "protos/perfetto/trace/etw/etw_event.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+namespace {
+
+using protozero::ConstBytes;
+
+}  // namespace
+EtwParser::EtwParser(TraceProcessorContext* context) : context_(context) {}
+
+util::Status EtwParser::ParseEtwEvent(uint32_t cpu,
+                                      int64_t ts,
+                                      const TracePacketData& data) {
+  using protos::pbzero::EtwTraceEvent;
+  const TraceBlobView& event = data.packet;
+  protos::pbzero::EtwTraceEvent::Decoder decoder(event.data(), event.length());
+
+  if (decoder.has_c_switch()) {
+    ParseCswitch(ts, cpu, decoder.c_switch());
+  }
+
+  if (decoder.has_ready_thread()) {
+    ParseReadyThread(ts, decoder.ready_thread());
+  }
+
+  return util::OkStatus();
+}
+
+void EtwParser::ParseCswitch(int64_t timestamp, uint32_t cpu, ConstBytes blob) {
+  protos::pbzero::CSwitchEtwEvent::Decoder cs(blob.data, blob.size);
+  PushSchedSwitch(cpu, timestamp, cs.old_thread_id(), cs.old_thread_state(),
+                  cs.new_thread_id(), cs.new_thread_priority());
+}
+
+void EtwParser::ParseReadyThread(int64_t timestamp, ConstBytes blob) {
+  protos::pbzero::ReadyThreadEtwEvent::Decoder rt(blob.data, blob.size);
+  UniqueTid utid =
+      context_->process_tracker->GetOrCreateThread(rt.t_thread_id());
+  ThreadStateTracker::GetOrCreate(context_)->PushWakingEvent(timestamp, utid,
+                                                             utid);
+}
+
+void EtwParser::PushSchedSwitch(uint32_t cpu,
+                                int64_t ts,
+                                uint32_t prev_tid,
+                                int64_t prev_state,
+                                uint32_t next_tid,
+                                int32_t next_prio) {
+  // At this stage all events should be globally timestamp ordered.
+  if (!context_->sched_event_tracker->UpdateEventTrackerTimestamp(
+          ts, "etw_cswitch", stats::sched_switch_out_of_order)) {
+    return;
+  }
+
+  UniqueTid next_utid = context_->process_tracker->GetOrCreateThread(next_tid);
+
+  // First use this data to close the previous slice.
+  bool prev_pid_match_prev_next_pid = false;
+  auto* pending_sched = sched_event_state_.GetPendingSchedInfoForCpu(cpu);
+  uint32_t pending_slice_idx = pending_sched->pending_slice_storage_idx;
+  StringId prev_state_string_id = TaskStateToStringId(prev_state);
+  if (prev_state_string_id == kNullStringId) {
+    context_->storage->IncrementStats(stats::task_state_invalid);
+  }
+  if (pending_slice_idx < std::numeric_limits<uint32_t>::max()) {
+    prev_pid_match_prev_next_pid = prev_tid == pending_sched->last_pid;
+    if (PERFETTO_LIKELY(prev_pid_match_prev_next_pid)) {
+      context_->sched_event_tracker->ClosePendingSlice(pending_slice_idx, ts,
+                                                       prev_state_string_id);
+    } else {
+      // If the pids are not consistent, make a note of this.
+      context_->storage->IncrementStats(stats::mismatched_sched_switch_tids);
+    }
+  }
+
+  auto new_slice_idx = context_->sched_event_tracker->AddStartSlice(
+      cpu, ts, next_utid, next_prio);
+
+  // Finally, update the info for the next sched switch on this CPU.
+  pending_sched->pending_slice_storage_idx = new_slice_idx;
+  pending_sched->last_pid = next_tid;
+  pending_sched->last_utid = next_utid;
+  pending_sched->last_prio = next_prio;
+
+  UniqueTid prev_utid = context_->process_tracker->GetOrCreateThread(prev_tid);
+
+  // Update the ThreadState table.
+  ThreadStateTracker::GetOrCreate(context_)->PushSchedSwitchEvent(
+      ts, cpu, prev_utid, prev_state_string_id, next_utid);
+}
+
+StringId EtwParser::TaskStateToStringId(int64_t task_state_int) {
+  const auto state = static_cast<uint8_t>(task_state_int);
+  // Mapping for the different Etw states with their string description.
+  std::map<uint8_t, base::StringView> etw_states_map = {
+      {0x00, "Initialized"},     // INITIALIZED
+      {0x01, "R"},               // READY
+      {0x02, "Running"},         // RUNNING
+      {0x03, "Stand By"},        // STANDBY
+      {0x04, "T"},               // TERMINATED
+      {0x05, "Waiting"},         // WAITING
+      {0x06, "Transition"},      // TRANSITION
+      {0x07, "Deferred Ready"},  // DEFERRED_READY
+  };
+
+  return etw_states_map.find(state) != etw_states_map.end()
+             ? context_->storage->InternString(etw_states_map[state])
+             : kNullStringId;
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/etw/etw_parser.h b/src/trace_processor/importers/etw/etw_parser.h
new file mode 100644
index 0000000..380c6e8
--- /dev/null
+++ b/src/trace_processor/importers/etw/etw_parser.h
@@ -0,0 +1,56 @@
+/*
+ * 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_PROCESSOR_IMPORTERS_ETW_ETW_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_PARSER_H_
+
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/status.h"
+#include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/common/sched_event_state.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class EtwParser {
+ public:
+  explicit EtwParser(TraceProcessorContext* context);
+
+  util::Status ParseEtwEvent(uint32_t cpu,
+                             int64_t ts,
+                             const TracePacketData& data);
+
+ private:
+  void ParseCswitch(int64_t timestamp, uint32_t cpu, protozero::ConstBytes);
+  void ParseReadyThread(int64_t timestamp, protozero::ConstBytes);
+  void PushSchedSwitch(uint32_t cpu,
+                       int64_t timestamp,
+                       uint32_t prev_pid,
+                       int64_t prev_state,
+                       uint32_t next_pid,
+                       int32_t next_prio);
+  StringId TaskStateToStringId(int64_t task_state_int);
+
+  TraceProcessorContext* context_;
+
+  SchedEventState sched_event_state_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ETW_ETW_PARSER_H_
diff --git a/src/trace_processor/importers/etw/etw_tokenizer.cc b/src/trace_processor/importers/etw/etw_tokenizer.cc
index f07d74c..2474454 100644
--- a/src/trace_processor/importers/etw/etw_tokenizer.cc
+++ b/src/trace_processor/importers/etw/etw_tokenizer.cc
@@ -50,9 +50,11 @@
   // in case the EtwTraceEvent does not contain the cpu.
   std::optional<uint32_t> bundle_cpu =
       decoder.has_cpu() ? std::make_optional(decoder.cpu()) : std::nullopt;
-  auto it = decoder.event();
-  return TokenizeEtwEvent(bundle_cpu, bundle.slice(it->data(), it->size()),
-                          state);
+
+  for (auto it = decoder.event(); it; ++it) {
+    TokenizeEtwEvent(bundle_cpu, bundle.slice(it->data(), it->size()), state);
+  }
+  return base::OkStatus();
 }
 
 PERFETTO_ALWAYS_INLINE
diff --git a/src/trace_processor/importers/etw/etw_tokenizer.h b/src/trace_processor/importers/etw/etw_tokenizer.h
index 1d93a14..6447ed2 100644
--- a/src/trace_processor/importers/etw/etw_tokenizer.h
+++ b/src/trace_processor/importers/etw/etw_tokenizer.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 3b846f4..0955a25 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<FtraceMessageDescriptor, 498> descriptors{{
+std::array<FtraceMessageDescriptor, 502> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -5501,6 +5501,52 @@
             {"prefree_seg", ProtoSchemaType::kUint32},
         },
     },
+    {
+        "fastrpc_dma_free",
+        3,
+        {
+            {},
+            {"cid", ProtoSchemaType::kInt32},
+            {"phys", ProtoSchemaType::kUint64},
+            {"size", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "fastrpc_dma_alloc",
+        5,
+        {
+            {},
+            {"cid", ProtoSchemaType::kInt32},
+            {"phys", ProtoSchemaType::kUint64},
+            {"size", ProtoSchemaType::kUint64},
+            {"attr", ProtoSchemaType::kUint64},
+            {"mflags", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "fastrpc_dma_unmap",
+        3,
+        {
+            {},
+            {"cid", ProtoSchemaType::kInt32},
+            {"phys", ProtoSchemaType::kUint64},
+            {"size", ProtoSchemaType::kUint64},
+        },
+    },
+    {
+        "fastrpc_dma_map",
+        7,
+        {
+            {},
+            {"cid", ProtoSchemaType::kInt32},
+            {"fd", ProtoSchemaType::kInt32},
+            {"phys", ProtoSchemaType::kUint64},
+            {"size", ProtoSchemaType::kUint64},
+            {"len", ProtoSchemaType::kUint64},
+            {"attr", ProtoSchemaType::kUint32},
+            {"mflags", ProtoSchemaType::kInt32},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
index 2c0a962..a7b1ced 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
@@ -74,8 +74,8 @@
                                         uint32_t next_pid,
                                         base::StringView next_comm,
                                         int32_t next_prio) {
-  if (!context_->sched_event_tracker->UpdateEventTrackerTimestamp(ts,
-      "sched_switch",stats::sched_switch_out_of_order)) {
+  if (!context_->sched_event_tracker->UpdateEventTrackerTimestamp(
+          ts, "sched_switch", stats::sched_switch_out_of_order)) {
     return;
   }
 
@@ -267,7 +267,6 @@
       ts, wakee_utid, curr_utid, common_flags);
 }
 
-PERFETTO_ALWAYS_INLINE
 void FtraceSchedEventTracker::AddRawSchedSwitchEvent(uint32_t cpu,
                                                      int64_t ts,
                                                      UniqueTid prev_utid,
diff --git a/src/trace_processor/importers/perf/perf_data_parser.cc b/src/trace_processor/importers/perf/perf_data_parser.cc
index 71912f2..83f8800 100644
--- a/src/trace_processor/importers/perf/perf_data_parser.cc
+++ b/src/trace_processor/importers/perf/perf_data_parser.cc
@@ -43,7 +43,7 @@
 
 base::StatusOr<PerfDataTracker::PerfSample> PerfDataParser::ParseSample(
     TraceBlobView tbv) {
-  perf_importer::Reader reader(std::move(tbv));
+  perf_importer::PerfDataReader reader(std::move(tbv));
   return tracker_->ParseSample(reader);
 }
 
diff --git a/src/trace_processor/importers/perf/perf_data_reader.cc b/src/trace_processor/importers/perf/perf_data_reader.cc
index a2c7691..e061826 100644
--- a/src/trace_processor/importers/perf/perf_data_reader.cc
+++ b/src/trace_processor/importers/perf/perf_data_reader.cc
@@ -25,7 +25,7 @@
 namespace perfetto {
 namespace trace_processor {
 namespace perf_importer {
-void Reader::SkipSlow(size_t bytes_to_skip) {
+void PerfDataReader::SkipSlow(size_t bytes_to_skip) {
   size_t bytes_in_buffer = BytesInBuffer();
 
   // Size fits in buffer.
@@ -40,7 +40,7 @@
   blob_offset_ += bytes_to_skip - bytes_in_buffer;
 }
 
-void Reader::PeekSlow(uint8_t* obj_data, size_t size) const {
+void PerfDataReader::PeekSlow(uint8_t* obj_data, size_t size) const {
   size_t bytes_in_buffer = BytesInBuffer();
 
   // Read from buffer.
@@ -55,7 +55,7 @@
          size - bytes_in_buffer);
 }
 
-TraceBlobView Reader::PeekTraceBlobViewSlow(size_t size) const {
+TraceBlobView PerfDataReader::PeekTraceBlobViewSlow(size_t size) const {
   auto blob = TraceBlob::Allocate(size);
   size_t bytes_in_buffer = BytesInBuffer();
 
diff --git a/src/trace_processor/importers/perf/perf_data_reader.h b/src/trace_processor/importers/perf/perf_data_reader.h
index 86f1c6b..acf07d1 100644
--- a/src/trace_processor/importers/perf/perf_data_reader.h
+++ b/src/trace_processor/importers/perf/perf_data_reader.h
@@ -35,10 +35,10 @@
 // it's design is not related to perf. Responsible for hiding away the
 // complexity of reading values from TraceBlobView and glueing the tbvs together
 // in case there is data between many of them.
-class Reader {
+class PerfDataReader {
  public:
-  Reader() = default;
-  explicit Reader(TraceBlobView tbv) : tbv_(std::move(tbv)) {}
+  PerfDataReader() = default;
+  explicit PerfDataReader(TraceBlobView tbv) : tbv_(std::move(tbv)) {}
 
   // Updates old TraceBlobView with new one. If there is data left in the old
   // one, it will be saved in the buffer.
diff --git a/src/trace_processor/importers/perf/perf_data_reader_unittest.cc b/src/trace_processor/importers/perf/perf_data_reader_unittest.cc
index 61c866c..5bf3081 100644
--- a/src/trace_processor/importers/perf/perf_data_reader_unittest.cc
+++ b/src/trace_processor/importers/perf/perf_data_reader_unittest.cc
@@ -37,7 +37,7 @@
 
 TEST(PerfDataReaderUnittest, AppendToEmpty) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{1, 2, 3});
-  Reader reader;
+  PerfDataReader reader;
   EXPECT_FALSE(reader.CanReadSize(1));
   reader.Append(std::move(tbv));
   EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 2));
@@ -45,7 +45,7 @@
 
 TEST(PerfDataReaderUnittest, Append) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{1, 2, 3});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
 
   EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 3));
   EXPECT_FALSE(reader.CanReadSize(sizeof(uint64_t) * 3 + 1));
@@ -56,7 +56,7 @@
 
 TEST(PerfDataReaderUnittest, Read) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   uint64_t val;
   reader.Read(val);
   EXPECT_EQ(val, 2u);
@@ -64,7 +64,7 @@
 
 TEST(PerfDataReaderUnittest, ReadFromBuffer) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 6});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3}));
 
   // Now the first vector should be in the buffer.
@@ -75,7 +75,7 @@
 
 TEST(PerfDataReaderUnittest, ReadBetweenBufferAndBlob) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   struct Nums {
@@ -94,7 +94,7 @@
 
 TEST(PerfDataReaderUnittest, ReadOptional) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   std::optional<uint64_t> val;
   reader.ReadOptional(val);
   EXPECT_EQ(val, 2u);
@@ -103,7 +103,7 @@
 TEST(PerfDataReaderUnittest, ReadVector) {
   TraceBlobView tbv =
       TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8, 16, 32});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
 
   std::vector<uint64_t> res(3);
   reader.ReadVector(res);
@@ -114,7 +114,7 @@
 
 TEST(PerfDataReaderUnittest, Skip) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
 
   reader.Skip<uint64_t>();
 
@@ -125,7 +125,7 @@
 
 TEST(PerfDataReaderUnittest, SkipInBuffer) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   reader.Skip<uint64_t>();
@@ -134,7 +134,7 @@
 
 TEST(PerfDataReaderUnittest, SkipBetweenBufferAndBlob) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   struct Nums {
@@ -149,7 +149,7 @@
 
 TEST(PerfDataReaderUnittest, Peek) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
 
   uint64_t peek_val;
   reader.Peek(peek_val);
@@ -161,7 +161,7 @@
 
 TEST(PerfDataReaderUnittest, PeekFromBuffer) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 6});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3}));
 
   uint64_t val;
@@ -171,7 +171,7 @@
 
 TEST(PerfDataReaderUnittest, PeekBetweenBufferAndBlob) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   struct Nums {
@@ -190,40 +190,40 @@
 
 TEST(PerfDataReaderUnittest, GetTraceBlobView) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 3));
 
   TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 2);
-  Reader new_reader(std::move(new_tbv));
+  PerfDataReader new_reader(std::move(new_tbv));
   EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 2));
   EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
 }
 
 TEST(PerfDataReaderUnittest, GetTraceBlobViewFromBuffer) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 2);
-  Reader new_reader(std::move(new_tbv));
+  PerfDataReader new_reader(std::move(new_tbv));
   EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 2));
   EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
 }
 
 TEST(PerfDataReaderUnittest, GetTraceBlobViewFromBetweenBufferAndBlob) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
 
   TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 3);
-  Reader new_reader(std::move(new_tbv));
+  PerfDataReader new_reader(std::move(new_tbv));
   EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
   EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 4));
 }
 
 TEST(PerfDataReaderUnittest, CanAccessFileRange) {
   TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  Reader reader(std::move(tbv));
+  PerfDataReader reader(std::move(tbv));
   EXPECT_TRUE(reader.CanAccessFileRange(2, sizeof(uint64_t) * 3));
   EXPECT_FALSE(reader.CanAccessFileRange(2, sizeof(uint64_t) * 3 + 10));
 }
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.h b/src/trace_processor/importers/perf/perf_data_tokenizer.h
index 1fa4277..7a54088 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.h
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.h
@@ -100,7 +100,7 @@
   uint64_t ids_end_ = 0;
   std::vector<uint8_t> after_header_buffer_;
 
-  perf_importer::Reader reader_;
+  perf_importer::PerfDataReader reader_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_data_tracker.cc b/src/trace_processor/importers/perf/perf_data_tracker.cc
index c670258..8b3bc47 100644
--- a/src/trace_processor/importers/perf/perf_data_tracker.cc
+++ b/src/trace_processor/importers/perf/perf_data_tracker.cc
@@ -101,7 +101,7 @@
 }
 
 base::StatusOr<PerfDataTracker::PerfSample> PerfDataTracker::ParseSample(
-    perfetto::trace_processor::perf_importer::Reader& reader) {
+    perfetto::trace_processor::perf_importer::PerfDataReader& reader) {
   uint64_t sample_type = common_sample_type();
   PerfDataTracker::PerfSample sample;
 
diff --git a/src/trace_processor/importers/perf/perf_data_tracker.h b/src/trace_processor/importers/perf/perf_data_tracker.h
index 11258ed..c96b08d 100644
--- a/src/trace_processor/importers/perf/perf_data_tracker.h
+++ b/src/trace_processor/importers/perf/perf_data_tracker.h
@@ -98,7 +98,7 @@
   uint64_t common_sample_type() { return common_sample_type_; }
 
   base::StatusOr<PerfSample> ParseSample(
-      perfetto::trace_processor::perf_importer::Reader&);
+      perfetto::trace_processor::perf_importer::PerfDataReader&);
 
  private:
   const perf_event_attr* FindAttrWithId(uint64_t id) const;
diff --git a/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc b/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc
index 129271c..923473f 100644
--- a/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc
+++ b/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc
@@ -121,7 +121,7 @@
 
   TraceBlob blob =
       TraceBlob::CopyFrom(static_cast<const void*>(&ts), sizeof(uint64_t));
-  Reader reader(TraceBlobView(std::move(blob)));
+  PerfDataReader reader(TraceBlobView(std::move(blob)));
 
   auto parsed_sample = tracker->ParseSample(reader);
   EXPECT_TRUE(parsed_sample.ok());
@@ -149,7 +149,7 @@
   memcpy(blob.data(), &sample.callchain_size, sizeof(uint64_t));
   memcpy(blob.data() + sizeof(uint64_t), sample.callchain.data(),
          sizeof(uint64_t) * 3);
-  Reader reader(TraceBlobView(std::move(blob)));
+  PerfDataReader reader(TraceBlobView(std::move(blob)));
 
   auto parsed_sample = tracker->ParseSample(reader);
   EXPECT_TRUE(parsed_sample.ok());
@@ -185,7 +185,7 @@
   memcpy(blob.data(), &sample, sizeof(Sample));
   memcpy(blob.data() + sizeof(Sample), callchain.data(), sizeof(uint64_t) * 3);
 
-  Reader reader(TraceBlobView(std::move(blob)));
+  PerfDataReader reader(TraceBlobView(std::move(blob)));
   EXPECT_TRUE(reader.CanReadSize(sizeof(Sample)));
 
   auto parsed_sample = tracker->ParseSample(reader);
@@ -229,7 +229,7 @@
   memcpy(blob.data(), &sample, sizeof(Sample));
   memcpy(blob.data() + sizeof(Sample), callchain.data(), sizeof(uint64_t) * 3);
 
-  Reader reader(TraceBlobView(std::move(blob)));
+  PerfDataReader reader(TraceBlobView(std::move(blob)));
 
   auto parsed_sample = tracker->ParseSample(reader);
   EXPECT_TRUE(parsed_sample.ok());
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index 0f2f53a..d40a84b 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -100,6 +100,7 @@
     "../../util:profiler_util",
     "../common",
     "../common:parser_types",
+    "../etw:minimal",
     "../ftrace:minimal",
     "../json:minimal",
     "../memory_tracker:graph_processor",
diff --git a/src/trace_processor/importers/proto/additional_modules.cc b/src/trace_processor/importers/proto/additional_modules.cc
index 83b13e5..91645c3 100644
--- a/src/trace_processor/importers/proto/additional_modules.cc
+++ b/src/trace_processor/importers/proto/additional_modules.cc
@@ -15,7 +15,7 @@
  */
 
 #include "src/trace_processor/importers/proto/additional_modules.h"
-#include "src/trace_processor/importers/etw/etw_module.h"
+#include "src/trace_processor/importers/etw/etw_module_impl.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module_impl.h"
 #include "src/trace_processor/importers/proto/android_camera_event_module.h"
 #include "src/trace_processor/importers/proto/android_probes_module.h"
@@ -45,13 +45,14 @@
   context->modules.emplace_back(new MetadataModule(context));
   context->modules.emplace_back(new V8Module(context));
   context->modules.emplace_back(new WinscopeModule(context));
-  context->modules.emplace_back(new EtwModule(context));
 
-  // Ftrace module is special, because it has one extra method for parsing
-  // ftrace packets. So we need to store a pointer to it separately.
+  // Ftrace/Etw modules are special, because it has one extra method for parsing
+  // ftrace/etw packets. So we need to store a pointer to it separately.
   context->modules.emplace_back(new FtraceModuleImpl(context));
   context->ftrace_module =
       static_cast<FtraceModule*>(context->modules.back().get());
+  context->modules.emplace_back(new EtwModuleImpl(context));
+  context->etw_module = static_cast<EtwModule*>(context->modules.back().get());
 
   if (context->multi_machine_trace_manager) {
     context->multi_machine_trace_manager->EnableAdditionalModules(
diff --git a/src/trace_processor/importers/proto/default_modules.cc b/src/trace_processor/importers/proto/default_modules.cc
index ca18beb..f7611d7 100644
--- a/src/trace_processor/importers/proto/default_modules.cc
+++ b/src/trace_processor/importers/proto/default_modules.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/importers/proto/default_modules.h"
+#include "src/trace_processor/importers/etw/etw_module.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/chrome_system_probes_module.h"
 #include "src/trace_processor/importers/proto/memory_tracker_snapshot_module.h"
@@ -27,11 +28,14 @@
 namespace trace_processor {
 
 void RegisterDefaultModules(TraceProcessorContext* context) {
+  // Ftrace and Etw modules are special, because they have an extra method for
+  // parsing the ftrace/etw packets. So we need to store a pointer to it
+  // separately.
   context->modules.emplace_back(new FtraceModule());
-  // Ftrace module is special, because it has one extra method for parsing
-  // ftrace packets. So we need to store a pointer to it separately.
   context->ftrace_module =
       static_cast<FtraceModule*>(context->modules.back().get());
+  context->modules.emplace_back(new EtwModule());
+  context->etw_module = static_cast<EtwModule*>(context->modules.back().get());
 
   context->modules.emplace_back(new TrackEventModule(context));
   context->track_module =
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 72c0f33..097f398 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -62,8 +62,9 @@
     return;
 
   auto* ref = storage->mutable_heap_graph_reference_table();
-  auto it =
-      ref->FilterToIterator({ref->reference_set_id().eq(*reference_set_id)});
+  Query q;
+  q.constraints = {ref->reference_set_id().eq(*reference_set_id)};
+  auto it = ref->FilterToIterator(q);
 
   for (; it; ++it) {
     if (!fn(it.row_reference()))
@@ -92,10 +93,10 @@
                                               uint32_t ref_set_id,
                                               const std::string& field_name) {
   const auto& refs_tbl = storage.heap_graph_reference_table();
-
-  auto refs_it = refs_tbl.FilterToIterator(
-      {refs_tbl.reference_set_id().eq(ref_set_id),
-       refs_tbl.field_name().eq(NullTermStringView(field_name))});
+  Query q;
+  q.constraints = {refs_tbl.reference_set_id().eq(ref_set_id),
+                   refs_tbl.field_name().eq(NullTermStringView(field_name))};
+  auto refs_it = refs_tbl.FilterToIterator(q);
   if (!refs_it) {
     return std::nullopt;
   }
@@ -110,8 +111,10 @@
   // Resolve superclasses by iterating heap graph objects and identifying the
   // superClass field.
   const auto& objects_tbl = storage->heap_graph_object_table();
-  auto obj_it = objects_tbl.FilterToIterator(
-      {objects_tbl.upid().eq(upid), objects_tbl.graph_sample_ts().eq(ts)});
+  Query q;
+  q.constraints = {objects_tbl.upid().eq(upid),
+                   objects_tbl.graph_sample_ts().eq(ts)};
+  auto obj_it = objects_tbl.FilterToIterator(q);
   for (; obj_it; ++obj_it) {
     auto obj_id = obj_it.id();
     auto class_descriptor = GetClassDescriptor(*storage, obj_id);
@@ -673,14 +676,16 @@
   };
   std::vector<Cleaner> cleaners;
 
-  auto class_it =
-      class_tbl.FilterToIterator({class_tbl.name().eq("sun.misc.Cleaner")});
+  Query q;
+  q.constraints = {class_tbl.name().eq("sun.misc.Cleaner")};
+  auto class_it = class_tbl.FilterToIterator(q);
   for (; class_it; ++class_it) {
     auto class_id = class_it.id();
-    auto obj_it = objects_tbl.FilterToIterator(
-        {objects_tbl.type_id().eq(class_id.value),
-         objects_tbl.upid().eq(seq.current_upid),
-         objects_tbl.graph_sample_ts().eq(seq.current_ts)});
+    Query query;
+    query.constraints = {objects_tbl.type_id().eq(class_id.value),
+                         objects_tbl.upid().eq(seq.current_upid),
+                         objects_tbl.graph_sample_ts().eq(seq.current_ts)};
+    auto obj_it = objects_tbl.FilterToIterator(query);
     for (; obj_it; ++obj_it) {
       ObjectTable::Id cleaner_obj_id = obj_it.id();
       std::optional<ObjectTable::Id> referent_id =
diff --git a/src/trace_processor/importers/proto/metadata_module.cc b/src/trace_processor/importers/proto/metadata_module.cc
index 9f2cb30..9011737 100644
--- a/src/trace_processor/importers/proto/metadata_module.cc
+++ b/src/trace_processor/importers/proto/metadata_module.cc
@@ -17,6 +17,7 @@
 #include "src/trace_processor/importers/proto/metadata_module.h"
 
 #include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/uuid.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
@@ -43,6 +44,7 @@
           context_->storage->InternString("trusted_producer_uid")) {
   RegisterForField(TracePacket::kUiStateFieldNumber, context);
   RegisterForField(TracePacket::kTriggerFieldNumber, context);
+  RegisterForField(TracePacket::kChromeTriggerFieldNumber, context);
   RegisterForField(TracePacket::kTraceUuidFieldNumber, context);
 }
 
@@ -128,7 +130,8 @@
   if (trigger.has_trigger_name()) {
     name_id = context_->storage->InternString(trigger.trigger_name());
   } else {
-    name_id = context_->storage->InternString(trigger.trigger_hash());
+    name_id = context_->storage->InternString(
+        base::StringView(base::IntToHexString(trigger.trigger_name_hash())));
   }
   context_->slice_tracker->Scoped(ts, track_id, cat_id, name_id,
                                   /* duration = */ 0);
diff --git a/src/trace_processor/importers/proto/network_trace_module_unittest.cc b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
index 79ba5de..0d9f53c 100644
--- a/src/trace_processor/importers/proto/network_trace_module_unittest.cc
+++ b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
@@ -70,7 +70,9 @@
   bool HasArg(ArgSetId sid, base::StringView key, Variadic value) {
     StringId key_id = storage_->InternString(key);
     const auto& a = storage_->arg_table();
-    for (auto it = a.FilterToIterator({a.arg_set_id().eq(sid)}); it; ++it) {
+    Query q;
+    q.constraints = {a.arg_set_id().eq(sid)};
+    for (auto it = a.FilterToIterator(q); it; ++it) {
       if (it.key() == key_id) {
         EXPECT_EQ(it.flat_key(), key_id);
         if (storage_->GetArgValue(it.row_number().row_number()) == value) {
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.cc b/src/trace_processor/importers/proto/proto_trace_parser.cc
index 4043ec6..d81ef57 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser.cc
@@ -36,6 +36,7 @@
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/etw/etw_module.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state.h"
 #include "src/trace_processor/importers/proto/track_event_module.h"
@@ -112,6 +113,18 @@
   context_->args_tracker->Flush();
 }
 
+void ProtoTraceParser::ParseEtwEvent(uint32_t cpu,
+                                     int64_t ts,
+                                     TracePacketData data) {
+  PERFETTO_DCHECK(context_->etw_module);
+  context_->etw_module->ParseEtwEventData(cpu, ts, data);
+
+  // TODO(lalitm): maybe move this to the flush method in the trace processor
+  // once we have it. This may reduce performance in the ArgsTracker though so
+  // needs to be handled carefully.
+  context_->args_tracker->Flush();
+}
+
 void ProtoTraceParser::ParseFtraceEvent(uint32_t cpu,
                                         int64_t ts,
                                         TracePacketData data) {
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.h b/src/trace_processor/importers/proto/proto_trace_parser.h
index bfe5602..621bd02 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser.h
@@ -49,6 +49,10 @@
   void ParseTrackEvent(int64_t ts, TrackEventData data) override;
   void ParseTracePacket(int64_t ts, TracePacketData data) override;
 
+  void ParseEtwEvent(uint32_t cpu,
+                     int64_t /*ts*/,
+                     TracePacketData data) override;
+
   void ParseFtraceEvent(uint32_t cpu,
                         int64_t /*ts*/,
                         TracePacketData data) override;
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
index 545418f..20e0085 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
@@ -298,7 +298,9 @@
 
   bool HasArg(ArgSetId set_id, StringId key_id, Variadic value) {
     const auto& args = storage_->arg_table();
-    RowMap rm = args.QueryToRowMap({args.arg_set_id().eq(set_id)}, {});
+    Query q;
+    q.constraints = {args.arg_set_id().eq(set_id)};
+    RowMap rm = args.QueryToRowMap(q);
     bool found = false;
     for (auto it = rm.IterateRows(); it; it.Next()) {
       if (args.key()[it.index()] == key_id) {
@@ -448,7 +450,9 @@
   auto set_id = raw.arg_set_id()[raw.row_count() - 1];
 
   const auto& args = storage_->arg_table();
-  RowMap rm = args.QueryToRowMap({args.arg_set_id().eq(set_id)}, {});
+  Query q;
+  q.constraints = {args.arg_set_id().eq(set_id)};
+  RowMap rm = args.QueryToRowMap(q);
 
   auto row = rm.Get(0);
 
diff --git a/src/trace_processor/importers/proto/winscope/BUILD.gn b/src/trace_processor/importers/proto/winscope/BUILD.gn
index 47e5cb4..93f9d1b 100644
--- a/src/trace_processor/importers/proto/winscope/BUILD.gn
+++ b/src/trace_processor/importers/proto/winscope/BUILD.gn
@@ -41,6 +41,8 @@
     "../../../../../protos/perfetto/trace/android:zero",
     "../../../../../protos/perfetto/trace/interned_data:zero",
     "../../../../../protos/perfetto/trace/profiling:zero",
+    "../../../../protozero",
+    "../../../containers",
     "../../../storage",
     "../../../tables",
     "../../../types",
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
index caf481d..1cfd530 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
@@ -14,16 +14,15 @@
  * limitations under the License.
  */
 
-#include "protolog_messages_tracker.h"
-#include "perfetto/ext/base/crash_keys.h"
-#include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/storage/metadata.h"
-#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 
-namespace perfetto {
-namespace trace_processor {
-ProtoLogMessagesTracker::ProtoLogMessagesTracker() {}
+#include <cstdint>
+#include <optional>
+#include <vector>
 
+namespace perfetto::trace_processor {
+
+ProtoLogMessagesTracker::ProtoLogMessagesTracker() = default;
 ProtoLogMessagesTracker::~ProtoLogMessagesTracker() = default;
 
 void ProtoLogMessagesTracker::TrackMessage(
@@ -34,18 +33,14 @@
       .first->emplace_back(tracked_protolog_message);
 }
 
-const std::optional<
-    std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
+std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
 ProtoLogMessagesTracker::GetTrackedMessagesByMessageId(uint64_t message_id) {
-  auto tracked_messages = tracked_protolog_messages.Find(message_id);
-
+  auto* tracked_messages = tracked_protolog_messages.Find(message_id);
   if (tracked_messages == nullptr) {
     // No tracked messages found for this id
     return std::nullopt;
   }
-
   return tracked_messages;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
index 0f47c32..de0285f 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
@@ -17,12 +17,18 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
 
-#include "perfetto/trace_processor/basic_types.h"
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
+#include "src/trace_processor/types/destructible.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class ProtoLogMessagesTracker : public Destructible {
  public:
@@ -41,16 +47,15 @@
   };
 
   static ProtoLogMessagesTracker* GetOrCreate(TraceProcessorContext* context) {
-    if (!context->shell_transitions_tracker) {
-      context->shell_transitions_tracker.reset(new ProtoLogMessagesTracker());
+    if (!context->protolog_messages_tracker) {
+      context->protolog_messages_tracker.reset(new ProtoLogMessagesTracker());
     }
     return static_cast<ProtoLogMessagesTracker*>(
-        context->shell_transitions_tracker.get());
+        context->protolog_messages_tracker.get());
   }
 
   void TrackMessage(TrackedProtoLogMessage tracked_protolog_message);
-  const std::optional<
-      std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
+  std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
   GetTrackedMessagesByMessageId(uint64_t message_id);
 
  private:
@@ -58,7 +63,6 @@
       tracked_protolog_messages;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.cc b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
index 15c19ac..a95777d 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
@@ -15,23 +15,33 @@
  */
 
 #include "src/trace_processor/importers/proto/winscope/protolog_parser.h"
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
 #include "protos/perfetto/trace/android/protolog.pbzero.h"
 #include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
 #include "protos/perfetto/trace/profiling/profile_common.pbzero.h"
 #include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 #include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
-#include "src/trace_processor/importers/proto/winscope/winscope_args_parser.h"
+#include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-#include <sstream>
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 enum ProtoLogLevel : int32_t {
   DEBUG = 1,
@@ -79,44 +89,27 @@
 
   std::vector<std::string> string_params;
   if (protolog_message.has_str_param_iids()) {
-    if (sequence_state->state()->IsIncrementalStateValid()) {
-      for (auto it = protolog_message.str_param_iids(); it; ++it) {
-        auto decoder =
-            sequence_state->state()
-                ->current_generation()
-                ->LookupInternedMessage<protos::pbzero::InternedData::
-                                            kProtologStringArgsFieldNumber,
-                                        protos::pbzero::InternedString>(
-                    it.field().as_uint32());
-
-        if (!decoder) {
-          // This shouldn't happen since we already checked the incremental
-          // state is valid.
-          string_params.emplace_back("<ERROR>");
-          context_->storage->IncrementStats(
-              stats::winscope_protolog_missing_interned_arg_parse_errors);
-          continue;
-        }
-
-        string_params.emplace_back(decoder->str().ToStdString());
+    for (auto it = protolog_message.str_param_iids(); it; ++it) {
+      auto* decoder = sequence_state->LookupInternedMessage<
+          protos::pbzero::InternedData::kProtologStringArgsFieldNumber,
+          protos::pbzero::InternedString>(it.field().as_uint32());
+      if (!decoder) {
+        // This shouldn't happen since we already checked the incremental
+        // state is valid.
+        string_params.emplace_back("<ERROR>");
+        context_->storage->IncrementStats(
+            stats::winscope_protolog_missing_interned_arg_parse_errors);
+        continue;
       }
-    } else {
-      // If the incremental state is not valid we will not be able to decode
-      // the interned strings correctly with 100% certainty so we will provide
-      // string parameters that are not decoded.
-      string_params.emplace_back("<MISSING_STR_ARG>");
+      string_params.emplace_back(decoder->str().ToStdString());
     }
   }
 
   std::optional<StringId> stacktrace = std::nullopt;
   if (protolog_message.has_stacktrace_iid()) {
-    auto stacktrace_decoder =
-        sequence_state->state()
-            ->current_generation()
-            ->LookupInternedMessage<
-                protos::pbzero::InternedData::kProtologStacktraceFieldNumber,
-                protos::pbzero::InternedString>(
-                protolog_message.stacktrace_iid());
+    auto* stacktrace_decoder = sequence_state->LookupInternedMessage<
+        protos::pbzero::InternedData::kProtologStacktraceFieldNumber,
+        protos::pbzero::InternedString>(protolog_message.stacktrace_iid());
 
     if (!stacktrace_decoder) {
       // This shouldn't happen since we already checked the incremental
@@ -135,7 +128,7 @@
   tables::ProtoLogTable::Row row;
   auto row_id = protolog_table->Insert(row).id;
 
-  auto protolog_message_tracker =
+  auto* protolog_message_tracker =
       ProtoLogMessagesTracker::GetOrCreate(context_);
   struct ProtoLogMessagesTracker::TrackedProtoLogMessage tracked_message = {
       protolog_message.message_id(),
@@ -154,13 +147,13 @@
 
   protos::pbzero::ProtoLogViewerConfig::Decoder protolog_viewer_config(blob);
 
-  std::unordered_map<uint32_t, std::string> group_tags;
+  base::FlatHashMap<uint32_t, std::string> group_tags;
   for (auto it = protolog_viewer_config.groups(); it; ++it) {
     protos::pbzero::ProtoLogViewerConfig::Group::Decoder group(*it);
-    group_tags.insert({group.id(), group.tag().ToStdString()});
+    group_tags.Insert(group.id(), group.tag().ToStdString());
   }
 
-  auto protolog_message_tracker =
+  auto* protolog_message_tracker =
       ProtoLogMessagesTracker::GetOrCreate(context_);
 
   for (auto it = protolog_viewer_config.messages(); it; ++it) {
@@ -172,7 +165,7 @@
             message_data.message_id());
 
     if (tracked_messages_opt.has_value()) {
-      auto group_tag = group_tags.find(message_data.group_id())->second;
+      auto* group_tag = group_tags.Find(message_data.group_id());
 
       for (const auto& tracked_message : *tracked_messages_opt.value()) {
         auto formatted_message = FormatMessage(
@@ -211,7 +204,8 @@
         }
         row.set_level(level);
 
-        auto tag = context_->storage->InternString(base::StringView(group_tag));
+        auto tag =
+            context_->storage->InternString(base::StringView(*group_tag));
         row.set_tag(tag);
 
         auto message = context_->storage->InternString(
@@ -227,7 +221,7 @@
 }
 
 std::string ProtoLogParser::FormatMessage(
-    const std::string message,
+    const std::string& message,
     const std::vector<int64_t>& sint64_params,
     const std::vector<double>& double_params,
     const std::vector<bool>& boolean_params,
@@ -282,13 +276,14 @@
           break;
         }
         case 's': {
-          formatted_message.append(str_params_itr->c_str());
+          formatted_message.append(*str_params_itr);
           ++str_params_itr;
           break;
-          case 'b':
-            formatted_message.append(*boolean_params_itr ? "true" : "false");
-            ++boolean_params_itr;
-            break;
+        }
+        case 'b': {
+          formatted_message.append(*boolean_params_itr ? "true" : "false");
+          ++boolean_params_itr;
+          break;
         }
         default:
           // Should never happen
@@ -306,5 +301,4 @@
   return formatted_message;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.h b/src/trace_processor/importers/proto/winscope/protolog_parser.h
index 97d9183..4c0db6f 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.h
@@ -17,15 +17,15 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_PARSER_H_
 
-#include "protos/perfetto/trace/android/protolog.pbzero.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
+#include <cstdint>
+#include <string>
+#include <vector>
+
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/util/descriptors.h"
 #include "src/trace_processor/util/proto_to_args_parser.h"
 
-namespace perfetto {
-
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -38,15 +38,12 @@
   void ParseProtoLogViewerConfig(protozero::ConstBytes);
 
  private:
-  std::string FormatMessage(const std::string message,
+  std::string FormatMessage(const std::string& message,
                             const std::vector<int64_t>& sint64_params,
                             const std::vector<double>& double_params,
                             const std::vector<bool>& boolean_params,
                             const std::vector<std::string>& string_params);
 
-  static constexpr auto* kProtoLogMessageProtoName =
-      "perfetto.protos.ProtoLogMessage";
-
   TraceProcessorContext* const context_;
   DescriptorPool pool_;
   util::ProtoToArgsParser args_parser_;
@@ -59,7 +56,6 @@
   const StringId log_level_wtf_string_id_;
   const StringId log_level_unknown_string_id_;
 };
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_PARSER_H_
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index bd77d0a..7de8cf6 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -56,6 +56,7 @@
     "android_multiuser.sql",
     "android_multiuser_populator.sql",
     "android_netperf.sql",
+    "android_oom_adjuster.sql",
     "android_other_traces.sql",
     "android_package_list.sql",
     "android_powrails.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_boot.sql b/src/trace_processor/metrics/sql/android/android_boot.sql
index 34d1c40..9140387 100644
--- a/src/trace_processor/metrics/sql/android/android_boot.sql
+++ b/src/trace_processor/metrics/sql/android/android_boot.sql
@@ -17,6 +17,7 @@
 INCLUDE PERFETTO MODULE android.process_metadata;
 INCLUDE PERFETTO MODULE android.app_process_starts;
 INCLUDE PERFETTO MODULE android.garbage_collection;
+INCLUDE PERFETTO MODULE android.oom_adjuster;
 
 CREATE OR REPLACE PERFETTO FUNCTION get_durations(process_name STRING)
 RETURNS TABLE(uint_sleep_dur LONG, total_dur LONG) AS
@@ -27,6 +28,55 @@
 INNER JOIN thread ON thread.upid=android_process_metadata.upid
 INNER JOIN thread_state ON thread.utid=thread_state.utid WHERE android_process_metadata.process_name=$process_name;
 
+CREATE OR REPLACE PERFETTO FUNCTION first_user_unlocked() RETURNS INT AS
+SELECT COALESCE(MIN(ts), 0) FROM thread_slice
+WHERE name GLOB "*android.intent.action.USER_UNLOCKED*";
+
+DROP TABLE IF EXISTS _oom_adj_events_with_src_bucket;
+CREATE PERFETTO TABLE _oom_adj_events_with_src_bucket
+AS
+SELECT
+  LAG(bucket) OVER (PARTITION BY upid ORDER BY ts) AS src_bucket,
+  ts,
+  bucket,
+  process_name,
+  oom_adj_reason
+FROM android_oom_adj_intervals;
+
+DROP VIEW IF EXISTS oom_adj_events_by_process_name;
+CREATE PERFETTO VIEW oom_adj_events_by_process_name AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  process_name
+FROM _oom_adj_events_with_src_bucket
+  WHERE ts > first_user_unlocked()
+GROUP BY process_name, bucket, src_bucket;
+
+DROP VIEW IF EXISTS oom_adj_events_global_by_bucket;
+CREATE PERFETTO VIEW oom_adj_events_global_by_bucket AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  NULL as name
+FROM _oom_adj_events_with_src_bucket
+WHERE
+  ts > first_user_unlocked()
+GROUP BY bucket, src_bucket;
+
+DROP VIEW IF EXISTS oom_adj_events_by_oom_adj_reason;
+CREATE PERFETTO VIEW oom_adj_events_by_oom_adj_reason AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  oom_adj_reason as name
+FROM _oom_adj_events_with_src_bucket
+WHERE ts > first_user_unlocked()
+GROUP BY bucket, src_bucket, oom_adj_reason;
+
 DROP VIEW IF EXISTS android_boot_output;
 CREATE PERFETTO VIEW android_boot_output AS
 SELECT AndroidBootMetric(
@@ -59,23 +109,23 @@
             'total_start_sum', (SELECT SUM(total_dur) FROM android_app_process_starts),
             'num_of_processes', (SELECT COUNT(*) FROM android_app_process_starts),
             'average_start_time', (SELECT AVG(total_dur) FROM android_app_process_starts)))
-            FROM android_app_process_starts),
+          FROM android_app_process_starts),
     'post_boot_process_start_aggregation', (
         SELECT NULL_IF_EMPTY(AndroidBootMetric_ProcessStartAggregation(
-            'total_start_sum', (SELECT SUM(total_dur) FROM android_app_process_starts
-              WHERE proc_start_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
+            'total_start_sum', (
+              SELECT SUM(total_dur)
+              FROM android_app_process_starts
+              WHERE proc_start_ts > first_user_unlocked()
             ),
-            'num_of_processes', (SELECT COUNT(*) FROM android_app_process_starts
-              WHERE proc_start_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+            'num_of_processes', (
+              SELECT COUNT(*)
+              FROM android_app_process_starts
+              WHERE proc_start_ts > first_user_unlocked()
             ),
-            'average_start_time', (SELECT AVG(total_dur) FROM android_app_process_starts
-              WHERE proc_start_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+            'average_start_time', (
+              SELECT AVG(total_dur)
+              FROM android_app_process_starts
+              WHERE proc_start_ts > first_user_unlocked()
             )
         ))
     ),
@@ -85,16 +135,23 @@
             ),
             'num_of_processes_with_gc', (SELECT COUNT(process_name) FROM android_garbage_collection_events
             ),
-            'num_of_threads_with_gc', (SELECT SUM(cnt) FROM (SELECT COUNT(*) AS cnt
-              FROM android_garbage_collection_events
-              GROUP by thread_name, process_name)
+            'num_of_threads_with_gc', (
+              SELECT SUM(cnt) FROM (
+                SELECT COUNT(*) AS cnt
+                FROM android_garbage_collection_events
+                GROUP by thread_name, process_name
+              )
             ),
             'avg_gc_duration', (SELECT AVG(gc_dur) FROM android_garbage_collection_events),
             'avg_running_gc_duration', (SELECT AVG(gc_running_dur) FROM android_garbage_collection_events),
-            'full_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
+            'full_gc_count', (
+              SELECT COUNT(*)
+              FROM android_garbage_collection_events
               WHERE gc_type = "full"
             ),
-            'collector_transition_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
+            'collector_transition_gc_count', (
+              SELECT COUNT(*)
+              FROM android_garbage_collection_events
               WHERE gc_type = "collector_transition"
             ),
             'young_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
@@ -117,68 +174,153 @@
     'post_boot_gc_aggregation', (
         SELECT NULL_IF_EMPTY(AndroidBootMetric_GarbageCollectionAggregation(
             'total_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
+              WHERE gc_ts > first_user_unlocked()
             ),
             'num_of_processes_with_gc', (SELECT COUNT(process_name) FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
+              WHERE gc_ts > first_user_unlocked()
             ),
             'num_of_threads_with_gc', (SELECT SUM(cnt) FROM (SELECT COUNT(*) AS cnt
               FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+              WHERE gc_ts > first_user_unlocked()
               GROUP by thread_name, process_name)
             ),
             'avg_gc_duration', (SELECT AVG(gc_dur) FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+              WHERE gc_ts > first_user_unlocked()
             ),
             'avg_running_gc_duration', (SELECT AVG(gc_running_dur) FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+              WHERE gc_ts > first_user_unlocked()
             ),
             'full_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "full" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
+              WHERE gc_type = "full" AND gc_ts > first_user_unlocked()
             ),
             'collector_transition_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "collector_transition" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
+              WHERE gc_type = "collector_transition" AND gc_ts > (
+                SELECT COALESCE(MIN(ts), 0)
+                FROM thread_slice
+                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
+                ORDER BY ts ASC LIMIT 1
+              )
             ),
-            'young_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "young" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
-            ),
-            'native_alloc_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "native_alloc" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
-            ),
-            'explicit_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "explicit_gc" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
-            ),
-            'alloc_gc_count', (SELECT COUNT(*) FROM android_garbage_collection_events
-              WHERE gc_type = "alloc_gc" AND gc_ts > (SELECT COALESCE(MIN(ts), 0)
-                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
-                ORDER BY ts ASC LIMIT 1 )
-            ),
-            'mb_per_ms_of_gc', (SELECT SUM(reclaimed_mb)/SUM(gc_running_dur/1e6) AS mb_per_ms_dur
+            'young_gc_count', (
+              SELECT COUNT(*)
               FROM android_garbage_collection_events
-              WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0) FROM thread_slice
-                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
-                ASC LIMIT 1 )
+              WHERE gc_type = "young" AND gc_ts > (
+                SELECT COALESCE(MIN(ts), 0)
+                FROM thread_slice
+                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
+                ORDER BY ts ASC LIMIT 1
+              )
+            ),
+            'native_alloc_gc_count', (
+              SELECT COUNT(*)
+              FROM android_garbage_collection_events
+              WHERE gc_type = "native_alloc" AND gc_ts > first_user_unlocked()
+            ),
+            'explicit_gc_count', (
+              SELECT COUNT(*) FROM android_garbage_collection_events
+              WHERE gc_type = "explicit_gc" AND gc_ts > (
+                SELECT COALESCE(MIN(ts), 0)
+                FROM thread_slice
+                WHERE name GLOB "*android.intent.action.USER_UNLOCKED*"
+                ORDER BY ts ASC LIMIT 1
+              )
+            ),
+            'alloc_gc_count', (
+              SELECT COUNT(*) FROM android_garbage_collection_events
+              WHERE gc_type = "alloc_gc" AND gc_ts > first_user_unlocked()
+            ),
+            'mb_per_ms_of_gc', (
+              SELECT
+                SUM(reclaimed_mb)/SUM(gc_running_dur/1e6) AS mb_per_ms_dur
+              FROM android_garbage_collection_events
+              WHERE gc_ts > first_user_unlocked()
             )
         ))
+    ),
+    'post_boot_oom_adjuster_transition_counts_by_process', (
+      SELECT RepeatedField(
+        AndroidBootMetric_OomAdjusterTransitionCounts(
+          'name', process_name,
+          'src_bucket', src_bucket,
+          'dest_bucket', bucket,
+          'count', count
+        )
+      ) FROM oom_adj_events_by_process_name
+    ),
+    'post_boot_oom_adjuster_transition_counts_global', (
+      SELECT RepeatedField(
+        AndroidBootMetric_OomAdjusterTransitionCounts(
+          'name', name,
+          'src_bucket', src_bucket,
+          'dest_bucket', bucket,
+          'count', count
+        )
+      )
+      FROM oom_adj_events_global_by_bucket
+    ),
+    'post_boot_oom_adjuster_transition_counts_by_oom_adj_reason',(
+      SELECT RepeatedField(
+        AndroidBootMetric_OomAdjusterTransitionCounts(
+          'name', name,
+          'src_bucket', src_bucket,
+          'dest_bucket', bucket,
+          'count', count
+        )
+      )
+      FROM oom_adj_events_by_oom_adj_reason
+    ),
+    'post_boot_oom_adj_bucket_duration_agg_global',(SELECT RepeatedField(
+      AndroidBootMetric_OomAdjBucketDurationAggregation(
+            'name', name,
+            'bucket', bucket,
+            'total_dur', total_dur
+      ))
+      FROM (
+        SELECT
+          NULL as name,
+          bucket,
+          SUM(dur) as total_dur
+        FROM android_oom_adj_intervals
+          WHERE ts > first_user_unlocked()
+        GROUP BY bucket)
+    ),
+    'post_boot_oom_adj_bucket_duration_agg_by_process',(SELECT RepeatedField(
+        AndroidBootMetric_OomAdjBucketDurationAggregation(
+            'name', name,
+            'bucket', bucket,
+            'total_dur', total_dur
+        )
     )
+    FROM (
+      SELECT
+        process_name as name,
+        bucket,
+        SUM(dur) as total_dur
+      FROM android_oom_adj_intervals
+      WHERE ts > first_user_unlocked()
+      AND process_name IS NOT NULL
+      GROUP BY process_name, bucket)
+    ),
+    'post_boot_oom_adj_duration_agg',
+    (SELECT RepeatedField(
+        AndroidBootMetric_OomAdjDurationAggregation(
+            'min_oom_adj_dur', min_oom_adj_dur,
+            'max_oom_adj_dur', max_oom_adj_dur,
+            'avg_oom_adj_dur', avg_oom_adj_dur,
+            'oom_adj_event_count', oom_adj_event_count,
+            'oom_adj_reason', oom_adj_reason
+        )
+    )
+    FROM (
+      SELECT
+        MIN(oom_adj_dur) as min_oom_adj_dur,
+        MAX(oom_adj_dur) as max_oom_adj_dur,
+        AVG(oom_adj_dur) as avg_oom_adj_dur,
+        COUNT(DISTINCT(oom_adj_id)) oom_adj_event_count,
+        oom_adj_reason
+      FROM android_oom_adj_intervals
+      WHERE ts > first_user_unlocked()
+      GROUP BY oom_adj_reason
+    )
+  )
 );
diff --git a/src/trace_processor/metrics/sql/android/android_oom_adjuster.sql b/src/trace_processor/metrics/sql/android/android_oom_adjuster.sql
new file mode 100644
index 0000000..7b752a8
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_oom_adjuster.sql
@@ -0,0 +1,139 @@
+--
+-- 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 android.oom_adjuster;
+
+DROP TABLE IF EXISTS _oom_adj_events_with_src_bucket;
+CREATE PERFETTO TABLE _oom_adj_events_with_src_bucket
+AS
+SELECT
+  LAG(bucket) OVER (PARTITION BY upid ORDER BY ts) AS src_bucket,
+  ts,
+  bucket,
+  process_name,
+  oom_adj_reason
+FROM android_oom_adj_intervals;
+
+DROP VIEW IF EXISTS oom_adj_events_by_process_name;
+CREATE PERFETTO VIEW oom_adj_events_by_process_name AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  process_name
+FROM _oom_adj_events_with_src_bucket
+GROUP BY process_name, bucket, src_bucket;
+
+DROP VIEW IF EXISTS oom_adj_events_global_by_bucket;
+CREATE PERFETTO VIEW oom_adj_events_global_by_bucket AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  NULL as name
+FROM _oom_adj_events_with_src_bucket
+GROUP BY bucket, src_bucket;
+
+DROP VIEW IF EXISTS oom_adj_events_by_oom_adj_reason;
+CREATE PERFETTO VIEW oom_adj_events_by_oom_adj_reason AS
+SELECT
+  src_bucket,
+  bucket,
+  count(ts) as count,
+  oom_adj_reason as name
+FROM _oom_adj_events_with_src_bucket
+GROUP BY bucket, src_bucket, oom_adj_reason;
+
+DROP VIEW IF EXISTS android_oom_adjuster_output;
+CREATE PERFETTO VIEW android_oom_adjuster_output AS
+SELECT AndroidOomAdjusterMetric(
+  'oom_adjuster_transition_counts_by_process', (
+    SELECT RepeatedField(
+      AndroidOomAdjusterMetric_OomAdjusterTransitionCounts(
+        'name', process_name,
+        'src_bucket', src_bucket,
+        'dest_bucket', bucket,
+        'count', count
+      )
+    ) FROM oom_adj_events_by_process_name
+  ),
+  'oom_adjuster_transition_counts_global', (
+    SELECT RepeatedField(
+      AndroidOomAdjusterMetric_OomAdjusterTransitionCounts(
+        'name', name,
+        'src_bucket', src_bucket,
+        'dest_bucket', bucket,
+        'count', count
+      )
+    )
+    FROM oom_adj_events_global_by_bucket
+  ),
+  'oom_adjuster_transition_counts_by_oom_adj_reason',(
+    SELECT RepeatedField(
+      AndroidOomAdjusterMetric_OomAdjusterTransitionCounts(
+        'name', name,
+        'src_bucket', src_bucket,
+        'dest_bucket', bucket,
+        'count', count
+      )
+    )
+    FROM oom_adj_events_by_oom_adj_reason
+  ),
+  'oom_adj_bucket_duration_agg_global',(SELECT RepeatedField(
+    AndroidOomAdjusterMetric_OomAdjBucketDurationAggregation(
+          'name', name,
+          'bucket', bucket,
+          'total_dur', total_dur
+        )
+    )
+    FROM (
+        SELECT NULL as name, bucket, SUM(dur) as total_dur
+        FROM android_oom_adj_intervals GROUP BY bucket
+    )
+  ),
+  'oom_adj_bucket_duration_agg_by_process',(SELECT RepeatedField(
+      AndroidOomAdjusterMetric_OomAdjBucketDurationAggregation(
+          'name', name,
+          'bucket', bucket,
+          'total_dur', total_dur
+      )
+    )
+    FROM (
+      SELECT process_name as name, bucket, SUM(dur) as total_dur
+      FROM android_oom_adj_intervals
+      WHERE process_name IS NOT NULL
+      GROUP BY process_name, bucket
+    )
+  ),
+  'oom_adj_duration_agg', (SELECT RepeatedField(
+      AndroidOomAdjusterMetric_OomAdjDurationAggregation(
+          'min_oom_adj_dur', min_oom_adj_dur,
+          'max_oom_adj_dur', max_oom_adj_dur,
+          'avg_oom_adj_dur', avg_oom_adj_dur,
+          'oom_adj_event_count', oom_adj_event_count,
+          'oom_adj_reason', oom_adj_reason
+      )
+    )
+    FROM (
+      SELECT
+        MIN(oom_adj_dur) as min_oom_adj_dur,
+        MAX(oom_adj_dur) as max_oom_adj_dur,
+        AVG(oom_adj_dur) as avg_oom_adj_dur,
+        COUNT(DISTINCT(oom_adj_id)) oom_adj_event_count,
+        oom_adj_reason
+      FROM android_oom_adj_intervals GROUP BY oom_adj_reason
+    )
+  )
+);
diff --git a/src/trace_processor/metrics/sql/android/java_heap_stats.sql b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
index dd2b7b6..1a4d534 100644
--- a/src/trace_processor/metrics/sql/android/java_heap_stats.sql
+++ b/src/trace_processor/metrics/sql/android/java_heap_stats.sql
@@ -109,6 +109,11 @@
     base_stats.upid,
     RepeatedField(JavaHeapStats_Sample(
       'ts', graph_sample_ts,
+      'process_uptime_ms',
+        CASE WHEN process.start_ts IS NOT NULL
+        THEN (graph_sample_ts - process.start_ts) / 1000000
+        ELSE NULL
+        END,
       'heap_size', total_size,
       'heap_native_size', total_native_size,
       'obj_count', total_obj_count,
@@ -120,6 +125,7 @@
       'oom_score_adj', closest_anon_swap_oom.oom_score_val
     )) AS sample_protos
   FROM base_stats
+  JOIN process USING (upid)
   LEFT JOIN closest_anon_swap_oom USING (upid, graph_sample_ts)
   GROUP BY 1
 )
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
index c9d69ca..94d2794 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
@@ -20,18 +20,23 @@
 -- 1 input per frame as flings are generated once per vsync.
 -- The numbers mentioned above are estimates in the ideal case scenario.
 
+INCLUDE PERFETTO MODULE chrome.scroll_jank.utils;
+INCLUDE PERFETTO MODULE common.slices;
+
 -- Grab all GestureScrollUpdate slices.
 DROP VIEW IF EXISTS chrome_all_scroll_updates;
 CREATE PERFETTO VIEW chrome_all_scroll_updates AS
 SELECT
-  id,
-  EXTRACT_ARG(arg_set_id, 'chrome_latency_info.gesture_scroll_id') AS scroll_id,
-  EXTRACT_ARG(arg_set_id, 'chrome_latency_info.is_coalesced') AS is_coalesced,
+  S.id,
+  chrome_get_most_recent_scroll_begin_id(ts) AS scroll_id,
+  has_descendant_slice_with_name(S.id, "SubmitCompositorFrameToPresentationCompositorFrame")
+  AS is_presented,
   ts,
   dur,
   track_id
-FROM slice
-WHERE name = "InputLatency::GestureScrollUpdate";
+FROM slice S JOIN args USING(arg_set_id)
+WHERE NAME = "EventLatency"
+AND args.string_value GLOB "*GESTURE_SCROLL_UPDATE";
 
 -- Count number of input GestureScrollUpdates per scroll.
 DROP VIEW IF EXISTS chrome_update_count_per_scroll;
@@ -46,26 +51,26 @@
 
 -- Count the number of input GestureScrollUpdates that were converted
 -- frames per scroll.
-DROP VIEW IF EXISTS chrome_non_coalesced_update_count_per_scroll;
-CREATE PERFETTO VIEW chrome_non_coalesced_update_count_per_scroll AS
+DROP VIEW IF EXISTS chrome_presented_update_count_per_scroll;
+CREATE PERFETTO VIEW chrome_presented_update_count_per_scroll AS
 SELECT
-  CAST(COUNT() AS FLOAT) AS non_coalesced_count,
+  CAST(COUNT() AS FLOAT) AS presented_count,
   scroll_id,
   id,
   track_id,
   dur
 FROM chrome_all_scroll_updates
-WHERE NOT is_coalesced
+WHERE is_presented
 GROUP BY scroll_id;
 
 -- Get the average number of inputs per frame per scroll.
 DROP VIEW IF EXISTS chrome_avg_scroll_inputs_per_frame;
 CREATE PERFETTO VIEW chrome_avg_scroll_inputs_per_frame AS
 SELECT
-  count / non_coalesced_count AS avg_inputs_per_frame_per_scroll,
+  count / presented_count AS avg_inputs_per_frame_per_scroll,
   scroll_id,
-  non_coalesced_count
-FROM chrome_non_coalesced_update_count_per_scroll
+  presented_count
+FROM chrome_presented_update_count_per_scroll
 JOIN chrome_update_count_per_scroll USING(scroll_id);
 
 -- Get the last scroll update event that wasn't coalesced before the
@@ -75,14 +80,14 @@
 SELECT
   id,
   scroll_id,
-  is_coalesced,
+  is_presented,
   ts,
   dur,
   track_id,
   (SELECT
     MAX(id)
     FROM chrome_all_scroll_updates parent_scrolls
-    WHERE NOT is_coalesced
+    WHERE is_presented
       AND parent_scrolls.ts <= scrolls.ts) AS presented_scroll_id
 FROM chrome_all_scroll_updates scrolls;
 
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 59e571f..387664e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
@@ -157,7 +157,9 @@
 
   // We assume that the row map is a contiguous range (which is always the case
   // because arg_set_ids are contiguous by definition).
-  row_map_ = args.QueryToRowMap({set_ids.eq(arg_set_id_)}, {});
+  Query q;
+  q.constraints = {set_ids.eq(arg_set_id_)};
+  row_map_ = args.QueryToRowMap(q);
   start_row_ = row_map_.empty() ? 0 : row_map_.Get(0);
 
   // If the vector already has entries, we've previously cached the mapping
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
index bbcb771..49a4a81 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
@@ -18,6 +18,10 @@
 
 source_set("operators") {
   sources = [
+    "counter_mipmap_operator.cc",
+    "counter_mipmap_operator.h",
+    "slice_mipmap_operator.cc",
+    "slice_mipmap_operator.h",
     "span_join_operator.cc",
     "span_join_operator.h",
     "window_operator.cc",
@@ -30,6 +34,7 @@
     "../../../../../include/perfetto/trace_processor",
     "../../../../../protos/perfetto/trace_processor:zero",
     "../../../../base",
+    "../../../containers",
     "../../../sqlite",
     "../../../util",
     "../../engine",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
new file mode 100644
index 0000000..83321ce
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
@@ -0,0 +1,265 @@
+/*
+ * 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_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h"
+
+#include <sqlite3.h>
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+constexpr char kSchema[] = R"(
+  CREATE TABLE x(
+    in_window_start BIGINT HIDDEN,
+    in_window_end BIGINT HIDDEN,
+    in_window_step BIGINT HIDDEN,
+    min_value DOUBLE,
+    max_value DOUBLE,
+    last_ts BIGINT,
+    last_value DOUBLE,
+    PRIMARY KEY(last_ts)
+  ) WITHOUT ROWID
+)";
+
+enum ColumnIndex : size_t {
+  kInWindowStart = 0,
+  kInWindowEnd,
+  kInWindowStep,
+
+  kMinValue,
+  kMaxValue,
+  kLastTs,
+  kLastValue,
+};
+
+constexpr size_t kArgCount = kInWindowStep + 1;
+
+bool IsArgColumn(size_t index) {
+  return index < kArgCount;
+}
+
+using Counter = CounterMipmapOperator::Counter;
+using Agg = CounterMipmapOperator::Agg;
+using Forest = ImplicitSegmentForest<Counter, Agg>;
+
+}  // namespace
+
+int CounterMipmapOperator::Create(sqlite3* db,
+                                  void* raw_ctx,
+                                  int argc,
+                                  const char* const* argv,
+                                  sqlite3_vtab** vtab,
+                                  char** zErr) {
+  if (argc != 4) {
+    *zErr = sqlite3_mprintf("counter_mipmap: wrong number of arguments");
+    return SQLITE_ERROR;
+  }
+
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+
+  auto* ctx = GetContext(raw_ctx);
+  auto state = std::make_unique<State>();
+
+  std::string sql = "SELECT ts, value FROM ";
+  sql.append(argv[3]);
+  auto res = ctx->engine->ExecuteUntilLastStatement(
+      SqlSource::FromTraceProcessorImplementation(std::move(sql)));
+  if (!res.ok()) {
+    *zErr = sqlite3_mprintf("%s", res.status().c_message());
+    return SQLITE_ERROR;
+  }
+  do {
+    int64_t ts = sqlite3_column_int64(res->stmt.sqlite_stmt(), 0);
+    auto value =
+        static_cast<float>(sqlite3_column_double(res->stmt.sqlite_stmt(), 1));
+    state->timestamps.push_back(ts);
+    state->forest.Push(Counter{value, value});
+  } while (res->stmt.Step());
+  if (!res->stmt.status().ok()) {
+    *zErr = sqlite3_mprintf("%s", res->stmt.status().c_message());
+    return SQLITE_ERROR;
+  }
+
+  std::unique_ptr<Vtab> vtab_res = std::make_unique<Vtab>();
+  vtab_res->state = ctx->manager.OnCreate(argv, std::move(state));
+  *vtab = vtab_res.release();
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Destroy(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<CounterMipmapOperator>::OnDestroy(tab->state);
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Connect(sqlite3* db,
+                                   void* raw_ctx,
+                                   int argc,
+                                   const char* const* argv,
+                                   sqlite3_vtab** vtab,
+                                   char**) {
+  PERFETTO_CHECK(argc == 4);
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  auto* ctx = GetContext(raw_ctx);
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->state = ctx->manager.OnConnect(argv);
+  *vtab = res.release();
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Disconnect(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<CounterMipmapOperator>::OnDisconnect(tab->state);
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::BestIndex(sqlite3_vtab*, sqlite3_index_info* info) {
+  base::Status status =
+      sqlite::utils::ValidateFunctionArguments(info, kArgCount, IsArgColumn);
+  if (!status.ok()) {
+    return SQLITE_CONSTRAINT;
+  }
+  if (info->nConstraint != kArgCount) {
+    return SQLITE_CONSTRAINT;
+  }
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Open(sqlite3_vtab*, sqlite3_vtab_cursor** cursor) {
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  *cursor = c.release();
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Close(sqlite3_vtab_cursor* cursor) {
+  std::unique_ptr<Cursor> c(GetCursor(cursor));
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Filter(sqlite3_vtab_cursor* cursor,
+                                  int,
+                                  const char*,
+                                  int argc,
+                                  sqlite3_value** argv) {
+  auto* c = GetCursor(cursor);
+  auto* t = GetVtab(c->pVtab);
+  auto* state =
+      sqlite::ModuleStateManager<CounterMipmapOperator>::GetState(t->state);
+  PERFETTO_CHECK(argc == kArgCount);
+
+  int64_t start_ts = sqlite3_value_int64(argv[0]);
+  int64_t end_ts = sqlite3_value_int64(argv[1]);
+  int64_t step_ts = sqlite3_value_int64(argv[2]);
+  if (start_ts == end_ts) {
+    return sqlite::utils::SetError(t, "counter_mipmap: empty range provided");
+  }
+
+  c->index = 0;
+  c->counters.clear();
+
+  // If there is a counter value before the start of this window, include it in
+  // the aggregation as well becaue it contributes to what should be rendered
+  // here.
+  auto ts_lb = std::lower_bound(state->timestamps.begin(),
+                                state->timestamps.end(), start_ts);
+  if (ts_lb != state->timestamps.begin() &&
+      (ts_lb == state->timestamps.end() || *ts_lb != start_ts)) {
+    --ts_lb;
+  }
+  int64_t start_idx = std::distance(state->timestamps.begin(), ts_lb);
+  for (int64_t s = start_ts; s < end_ts; s += step_ts) {
+    int64_t end_idx =
+        std::distance(state->timestamps.begin(),
+                      std::lower_bound(state->timestamps.begin() +
+                                           static_cast<int64_t>(start_idx),
+                                       state->timestamps.end(), s + step_ts));
+    if (start_idx == end_idx) {
+      continue;
+    }
+    c->counters.emplace_back(Cursor::Result{
+        state->forest.Query(static_cast<uint32_t>(start_idx),
+                            static_cast<uint32_t>(end_idx)),
+        state->forest[static_cast<uint32_t>(end_idx) - 1],
+        state->timestamps[static_cast<uint32_t>(end_idx) - 1],
+    });
+    start_idx = end_idx;
+  }
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Next(sqlite3_vtab_cursor* cursor) {
+  GetCursor(cursor)->index++;
+  return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Eof(sqlite3_vtab_cursor* cursor) {
+  auto* c = GetCursor(cursor);
+  return c->index >= c->counters.size();
+}
+
+int CounterMipmapOperator::Column(sqlite3_vtab_cursor* cursor,
+                                  sqlite3_context* ctx,
+                                  int N) {
+  auto* t = GetVtab(cursor->pVtab);
+  auto* c = GetCursor(cursor);
+  const auto& res = c->counters[c->index];
+  switch (N) {
+    case ColumnIndex::kMinValue:
+      sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.min));
+      return SQLITE_OK;
+    case ColumnIndex::kMaxValue:
+      sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.max));
+      return SQLITE_OK;
+    case ColumnIndex::kLastTs:
+      sqlite::result::Long(ctx, res.last_ts);
+      return SQLITE_OK;
+    case ColumnIndex::kLastValue:
+      PERFETTO_DCHECK(
+          std::equal_to<>()(res.last_counter.min, res.last_counter.max));
+      sqlite::result::Double(ctx, static_cast<double>(res.last_counter.min));
+      return SQLITE_OK;
+    default:
+      return sqlite::utils::SetError(t, "Bad column");
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+int CounterMipmapOperator::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
new file mode 100644
index 0000000..1b0897c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
@@ -0,0 +1,127 @@
+/*
+ * 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_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
+
+#include <sqlite3.h>
+#include <cstdint>
+#include <vector>
+
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+
+namespace perfetto::trace_processor {
+
+// Operator for building "mipmaps" [1] over the counter-like tracks.
+//
+// In the context of trace data, mipmap really means aggregating the counter
+// values in a given time period into the {min, max, last} value for that
+// period, allowing UIs to efficiently display the contents of a counter track
+// when very zoomed out.
+//
+// Specifically, we are computing the query:
+// ```
+//   select
+//     last_value(ts),
+//     min(value),
+//     max(value),
+//     last_value(value)
+//   from $input in
+//   where in.ts_end >= $window_start and in.ts <= $window_end
+//   group by ts / $window_resolution
+//   order by ts
+// ```
+// but in O(logn) time by using a segment-tree like data structure (see
+// ImplicitSegmentForest).
+//
+// [1] https://en.wikipedia.org/wiki/Mipmap
+struct CounterMipmapOperator : sqlite::Module<CounterMipmapOperator> {
+  struct Counter {
+    float min;
+    float max;
+  };
+  struct Agg {
+    Counter operator()(const Counter& a, const Counter& b) {
+      Counter res;
+      res.min = b.min < a.min ? b.min : a.min;
+      res.max = b.max > a.max ? b.max : a.max;
+      return res;
+    }
+  };
+  struct State {
+    ImplicitSegmentForest<Counter, Agg> forest;
+    std::vector<int64_t> timestamps;
+  };
+  struct Context {
+    explicit Context(PerfettoSqlEngine* _engine) : engine(_engine) {}
+    PerfettoSqlEngine* engine;
+    sqlite::ModuleStateManager<CounterMipmapOperator> manager;
+  };
+  struct Vtab : sqlite::Module<CounterMipmapOperator>::Vtab {
+    sqlite::ModuleStateManager<CounterMipmapOperator>::PerVtabState* state;
+  };
+  struct Cursor : sqlite::Module<CounterMipmapOperator>::Cursor {
+    struct Result {
+      Counter min_max_counter;
+      Counter last_counter;
+      int64_t last_ts;
+    };
+    std::vector<Result> counters;
+    uint32_t index;
+  };
+
+  static constexpr auto kType = kCreateOnly;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
+
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+  static int Destroy(sqlite3_vtab*);
+
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
+
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
+
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc
new file mode 100644
index 0000000..6fe6956
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.cc
@@ -0,0 +1,275 @@
+/*
+ * 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_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h"
+
+#include <sqlite3.h>
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/public/compiler.h"
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+constexpr char kSliceSchema[] = R"(
+  CREATE TABLE x(
+    in_window_start BIGINT HIDDEN,
+    in_window_end BIGINT HIDDEN,
+    in_window_step BIGINT HIDDEN,
+    ts BIGINT,
+    id BIGINT,
+    dur BIGINT,
+    depth INTEGER,
+    PRIMARY KEY(id)
+  ) WITHOUT ROWID
+)";
+
+enum ColumnIndex : size_t {
+  kInWindowStart = 0,
+  kInWindowEnd,
+  kInWindowStep,
+
+  kTs,
+  kId,
+  kDur,
+  kDepth,
+};
+
+constexpr size_t kArgCount = kInWindowStep + 1;
+
+bool IsArgColumn(size_t index) {
+  return index < kArgCount;
+}
+
+}  // namespace
+
+int SliceMipmapOperator::Create(sqlite3* db,
+                                void* raw_ctx,
+                                int argc,
+                                const char* const* argv,
+                                sqlite3_vtab** vtab,
+                                char** zErr) {
+  if (argc != 4) {
+    *zErr = sqlite3_mprintf("zoom_index_operator: wrong number of arguments");
+    return SQLITE_ERROR;
+  }
+
+  if (int ret = sqlite3_declare_vtab(db, kSliceSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+
+  auto* ctx = GetContext(raw_ctx);
+  auto state = std::make_unique<State>();
+
+  std::string sql = "SELECT * FROM ";
+  sql.append(argv[3]);
+  auto res = ctx->engine->ExecuteUntilLastStatement(
+      SqlSource::FromTraceProcessorImplementation(std::move(sql)));
+  if (!res.ok()) {
+    *zErr =
+        sqlite3_mprintf("zoom_index_operator: %s", res.status().c_message());
+    return SQLITE_ERROR;
+  }
+  do {
+    auto id =
+        static_cast<uint32_t>(sqlite3_column_int64(res->stmt.sqlite_stmt(), 0));
+    int64_t ts = sqlite3_column_int64(res->stmt.sqlite_stmt(), 1);
+    int64_t dur = sqlite3_column_int64(res->stmt.sqlite_stmt(), 2);
+    auto depth =
+        static_cast<uint32_t>(sqlite3_column_int64(res->stmt.sqlite_stmt(), 3));
+    if (PERFETTO_UNLIKELY(depth >= state->by_depth.size())) {
+      state->by_depth.resize(depth + 1);
+    }
+    auto& by_depth = state->by_depth[depth];
+    by_depth.forest.Push(
+        Slice{dur, id, static_cast<uint32_t>(by_depth.forest.size())});
+    by_depth.timestamps.push_back(ts);
+  } while (res->stmt.Step());
+  if (!res->stmt.status().ok()) {
+    *zErr = sqlite3_mprintf("zoom_index_operator: %s",
+                            res->stmt.status().c_message());
+    return SQLITE_ERROR;
+  }
+
+  std::unique_ptr<Vtab> vtab_res = std::make_unique<Vtab>();
+  vtab_res->state = ctx->manager.OnCreate(argv, std::move(state));
+  *vtab = vtab_res.release();
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Destroy(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<SliceMipmapOperator>::OnDestroy(tab->state);
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Connect(sqlite3* db,
+                                 void* raw_ctx,
+                                 int argc,
+                                 const char* const* argv,
+                                 sqlite3_vtab** vtab,
+                                 char**) {
+  PERFETTO_CHECK(argc == 4);
+  if (int ret = sqlite3_declare_vtab(db, kSliceSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  auto* ctx = GetContext(raw_ctx);
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->state = ctx->manager.OnConnect(argv);
+  *vtab = res.release();
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Disconnect(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<SliceMipmapOperator>::OnDisconnect(tab->state);
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::BestIndex(sqlite3_vtab*, sqlite3_index_info* info) {
+  base::Status status =
+      sqlite::utils::ValidateFunctionArguments(info, kArgCount, IsArgColumn);
+  if (!status.ok()) {
+    return SQLITE_CONSTRAINT;
+  }
+  if (info->nConstraint != kArgCount) {
+    return SQLITE_CONSTRAINT;
+  }
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Open(sqlite3_vtab*, sqlite3_vtab_cursor** cursor) {
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  *cursor = c.release();
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Close(sqlite3_vtab_cursor* cursor) {
+  std::unique_ptr<Cursor> c(GetCursor(cursor));
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Filter(sqlite3_vtab_cursor* cursor,
+                                int,
+                                const char*,
+                                int argc,
+                                sqlite3_value** argv) {
+  auto* c = GetCursor(cursor);
+  auto* t = GetVtab(c->pVtab);
+  auto* state =
+      sqlite::ModuleStateManager<SliceMipmapOperator>::GetState(t->state);
+  PERFETTO_CHECK(argc == kArgCount);
+
+  c->results.clear();
+  c->index = 0;
+
+  int64_t start = sqlite3_value_int64(argv[0]);
+  int64_t end = sqlite3_value_int64(argv[1]);
+  int64_t step = sqlite3_value_int64(argv[2]);
+
+  if (start == end) {
+    return sqlite::utils::SetError(t, "slice_mipmap: empty range provided");
+  }
+
+  for (uint32_t depth = 0; depth < state->by_depth.size(); ++depth) {
+    auto& by_depth = state->by_depth[depth];
+    const auto& tses = by_depth.timestamps;
+
+    // If the slice before this window overlaps with the current window, move
+    // the iterator back one to consider it as well.
+    auto start_idx = static_cast<uint32_t>(std::distance(
+        tses.begin(), std::lower_bound(tses.begin(), tses.end(), start)));
+    if (start_idx != 0 &&
+        (static_cast<size_t>(start_idx) == tses.size() ||
+         tses[start_idx] != start) &&
+        (tses[start_idx] + by_depth.forest[start_idx].dur > start)) {
+      --start_idx;
+    }
+
+    for (int64_t s = start; s < end; s += step) {
+      auto end_idx = static_cast<uint32_t>(std::distance(
+          tses.begin(),
+          std::lower_bound(tses.begin() + static_cast<int64_t>(start_idx),
+                           tses.end(), s + step)));
+      if (start_idx == end_idx) {
+        continue;
+      }
+      auto res = by_depth.forest.Query(start_idx, end_idx);
+      c->results.emplace_back(Cursor::Result{
+          tses[res.idx],
+          res.dur,
+          res.id,
+          depth,
+      });
+      start_idx = end_idx;
+    }
+  }
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Next(sqlite3_vtab_cursor* cursor) {
+  GetCursor(cursor)->index++;
+  return SQLITE_OK;
+}
+
+int SliceMipmapOperator::Eof(sqlite3_vtab_cursor* cursor) {
+  auto* c = GetCursor(cursor);
+  return c->index >= c->results.size();
+}
+
+int SliceMipmapOperator::Column(sqlite3_vtab_cursor* cursor,
+                                sqlite3_context* ctx,
+                                int N) {
+  auto* t = GetVtab(cursor->pVtab);
+  auto* c = GetCursor(cursor);
+  switch (N) {
+    case ColumnIndex::kTs:
+      sqlite::result::Long(ctx, c->results[c->index].timestamp);
+      return SQLITE_OK;
+    case ColumnIndex::kId:
+      sqlite::result::Long(ctx, c->results[c->index].id);
+      return SQLITE_OK;
+    case ColumnIndex::kDur:
+      sqlite::result::Long(ctx, c->results[c->index].dur);
+      return SQLITE_OK;
+    case ColumnIndex::kDepth:
+      sqlite::result::Long(ctx, c->results[c->index].depth);
+      return SQLITE_OK;
+    default:
+      return sqlite::utils::SetError(t, "Bad column");
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+int SliceMipmapOperator::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h
new file mode 100644
index 0000000..3291225
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h
@@ -0,0 +1,128 @@
+/*
+ * 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_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_SLICE_MIPMAP_OPERATOR_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_SLICE_MIPMAP_OPERATOR_H_
+
+#include <sqlite3.h>
+#include <cstdint>
+#include <vector>
+
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+
+namespace perfetto::trace_processor {
+
+// Operator for building "mipmaps"  [1] over the slices in the trace.
+//
+// In this context mipmap really means aggregating the slices in a given
+// time period by max(dur) for that period, allowing UIs to efficiently display
+// the contents of slice tracks when very zoomed out.
+//
+// Specifically, we are computing the query:
+// ```
+//   select
+//     depth,
+//     max(dur) as dur,
+//     id,
+//     ts
+//   from $input in
+//   where in.ts_end >= $window_start and in.ts <= $window_end
+//   group by depth, ts / $window_resolution
+//   order by ts
+// ```
+// but in O(logn) time by using a segment-tree like data structure (see
+// ImplicitSegmentForest).
+//
+// [1] https://en.wikipedia.org/wiki/Mipmap
+struct SliceMipmapOperator : sqlite::Module<SliceMipmapOperator> {
+  struct Slice {
+    int64_t dur;
+    uint32_t id;
+    uint32_t idx;
+  };
+  struct Agg {
+    Slice operator()(const Slice& a, const Slice& b) {
+      return a.dur < b.dur ? b : a;
+    }
+  };
+  struct PerDepth {
+    ImplicitSegmentForest<Slice, Agg> forest;
+    std::vector<int64_t> timestamps;
+  };
+  struct State {
+    std::vector<PerDepth> by_depth;
+  };
+  struct Context {
+    explicit Context(PerfettoSqlEngine* _engine) : engine(_engine) {}
+    PerfettoSqlEngine* engine;
+    sqlite::ModuleStateManager<SliceMipmapOperator> manager;
+  };
+  struct Vtab : sqlite::Module<SliceMipmapOperator>::Vtab {
+    sqlite::ModuleStateManager<SliceMipmapOperator>::PerVtabState* state;
+  };
+  struct Cursor : sqlite::Module<SliceMipmapOperator>::Cursor {
+    struct Result {
+      int64_t timestamp;
+      int64_t dur;
+      uint32_t id;
+      uint32_t depth;
+    };
+    std::vector<Result> results;
+    uint32_t index = 0;
+  };
+
+  static constexpr auto kType = kCreateOnly;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
+
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+  static int Destroy(sqlite3_vtab*);
+
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
+
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
+
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_SLICE_MIPMAP_OPERATOR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
index f102b1f..ab9ea14 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
@@ -142,8 +142,9 @@
       // Find the all slice ids that have the stack id and find all the
       // ancestors of the slice ids.
       const auto& slice_table = storage_->slice_table();
-      auto it =
-          slice_table.FilterToIterator({slice_table.stack_id().eq(start_id)});
+      Query q;
+      q.constraints = {slice_table.stack_id().eq(start_id)};
+      auto it = slice_table.FilterToIterator(q);
       std::vector<tables::SliceTable::RowNumber> ancestors;
       for (; it; ++it) {
         RETURN_IF_ERROR(GetAncestors(slice_table, it.id(), ancestors));
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.cc
index 1e4f10e..4089036 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow.cc
@@ -159,7 +159,9 @@
     const TypedColumn<SliceId>& start_col =
         flow_direction == FlowDirection::OUTGOING ? flow.slice_out()
                                                   : flow.slice_in();
-    auto it = flow.FilterToIterator({start_col.eq(slice_id.value)});
+    Query q;
+    q.constraints = {start_col.eq(slice_id.value)};
+    auto it = flow.FilterToIterator(q);
     for (; it; ++it) {
       flow_rows_.push_back(it.row_number());
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc
index 8f3ec43..d1b965f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc
@@ -77,21 +77,23 @@
   // track are always perfectly stacked).
   // For unfinshed slices (i.e. -1 dur), we need to consider until the end of
   // the trace so we cannot add any similar constraint.
-  std::vector<Constraint> cs;
+  Query q;
   if (start_ref->dur() >= 0) {
-    cs.emplace_back(slices.ts().le(start_ref->ts() + start_ref->dur()));
+    q.constraints.emplace_back(
+        slices.ts().le(start_ref->ts() + start_ref->dur()));
   }
 
   // All nested descendents must be on the same track, with a ts greater than
   // |start_ref.ts| and whose depth is larger than |start_ref|'s.
-  cs.emplace_back(slices.ts().ge(start_ref->ts()));
-  cs.emplace_back(slices.track_id().eq(start_ref->track_id().value));
-  cs.emplace_back(slices.depth().gt(start_ref->depth()));
+  q.constraints.emplace_back(slices.ts().ge(start_ref->ts()));
+  q.constraints.emplace_back(slices.track_id().eq(start_ref->track_id().value));
+  q.constraints.emplace_back(slices.depth().gt(start_ref->depth()));
 
   // It's important we insert directly into |row_numbers_accumulator| and not
   // overwrite it because we expect the existing elements in
   // |row_numbers_accumulator| to be preserved.
-  for (auto it = slices.FilterToIterator(cs); it; ++it) {
+
+  for (auto it = slices.FilterToIterator(q); it; ++it) {
     row_numbers_accumulator.emplace_back(it.row_number());
   }
   return base::OkStatus();
@@ -136,7 +138,7 @@
     }
     case Type::kSliceByStack: {
       auto sbs_cs = {slices.stack_id().eq(start_id)};
-      for (auto it = slices.FilterToIterator(sbs_cs); it; ++it) {
+      for (auto it = slices.FilterToIterator(Query{sbs_cs, {}}); it; ++it) {
         RETURN_IF_ERROR(GetDescendants(slices, it.id(), descendants));
       }
       return ExtendWithStartId<tables::DescendantSliceByStackTable>(
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc
index c22362d..3a107d2 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc
@@ -100,8 +100,9 @@
   inserter.Populate(table);
 
   auto out = ExperimentalFlatSlice::ComputeFlatSliceTable(table, &pool, 0, 400);
-  auto it = out->ApplyAndIterateRows(out->QueryToRowMap(
-      {}, {out->track_id().ascending(), out->ts().ascending()}));
+  Query q;
+  q.orders = {out->track_id().ascending(), out->ts().ascending()};
+  auto it = out->ApplyAndIterateRows(out->QueryToRowMap(q));
 
   TableAsserter asserter(std::move(it));
 
@@ -183,8 +184,9 @@
 
   auto out =
       ExperimentalFlatSlice::ComputeFlatSliceTable(table, &pool, start, end);
-  auto it = out->ApplyAndIterateRows(out->QueryToRowMap(
-      {}, {out->track_id().ascending(), out->ts().ascending()}));
+  Query q;
+  q.orders = {out->track_id().ascending(), out->ts().ascending()};
+  auto it = out->ApplyAndIterateRows(out->QueryToRowMap(q));
 
   TableAsserter asserter(std::move(it));
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc
index 56c3bf8..4ad0b9a 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc
@@ -330,8 +330,10 @@
       storage->heap_profile_allocation_table();
   // PASS OVER ALLOCATIONS:
   // Aggregate allocations into the newly built tree.
-  auto it = allocation_tbl.FilterToIterator(
-      {allocation_tbl.ts().le(timestamp), allocation_tbl.upid().eq(upid)});
+  Query q;
+  q.constraints = {allocation_tbl.ts().le(timestamp),
+                   allocation_tbl.upid().eq(upid)};
+  auto it = allocation_tbl.FilterToIterator(q);
   if (!it) {
     return nullptr;
   }
@@ -367,7 +369,7 @@
   std::unordered_set<UniqueTid> utids;
   {
     auto it = storage->thread_table().FilterToIterator(
-        {storage->thread_table().upid().is_not_null()});
+        Query{{storage->thread_table().upid().is_not_null()}, {}});
     for (; it; ++it) {
       if (upids.count(*it.upid())) {
         utids.emplace(it.id().value);
@@ -391,7 +393,7 @@
   }
   std::vector<uint32_t> cs_rows;
   {
-    auto it = storage->perf_sample_table().FilterToIterator(cs);
+    auto it = storage->perf_sample_table().FilterToIterator(Query{cs, {}});
     for (; it; ++it) {
       if (utids.find(it.utid()) != utids.end()) {
         cs_rows.push_back(it.row_number().row_number());
@@ -404,10 +406,10 @@
   }
 
   // The logic underneath is selecting a default timestamp to be used by all
-  // frames which do not have a timestamp. The timestamp is taken from the query
-  // value and it's not meaningful for the row. It prevents however the rows
-  // with no timestamp from being filtered out by Sqlite, after we create the
-  // table ExperimentalFlamegraphTable in this class.
+  // frames which do not have a timestamp. The timestamp is taken from the
+  // query value and it's not meaningful for the row. It prevents however the
+  // rows with no timestamp from being filtered out by Sqlite, after we create
+  // the table ExperimentalFlamegraphTable in this class.
   int64_t default_timestamp = 0;
   if (!time_constraints.empty()) {
     auto& tc = time_constraints[0];
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index 4c492d1..349e2f6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -36,6 +36,7 @@
     "stack_trace",
     "time",
     "v8",
+    "viz/summary",
     "wattson",
   ]
   generated_header = "stdlib.h"
diff --git a/src/trace_processor/perfetto_sql/stdlib/OWNERS b/src/trace_processor/perfetto_sql/stdlib/OWNERS
index 0479e4b..2d6db69 100644
--- a/src/trace_processor/perfetto_sql/stdlib/OWNERS
+++ b/src/trace_processor/perfetto_sql/stdlib/OWNERS
@@ -1,3 +1,5 @@
-# These can't be added inside chrome/ bacause the whole directory is
-# blown away on every import.
+# These can't be added inside chrome/ because the whole directory is
+# blown away on every import. Subdirectories of `chrome` will also
+# need CHROMIUM_OWNERS added here.
 per-file chrome/* = file://protos/third_party/CHROMIUM_OWNERS
+per-file chrome/scroll_jank/* = file://protos/third_party/CHROMIUM_OWNERS
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/frames/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/frames/BUILD.gn
index bc434d5..306758a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/BUILD.gn
@@ -18,5 +18,6 @@
   sources = [
     "per_frame_metrics.sql",
     "timeline.sql",
+    "timeline_maxsdk28.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
index b492bb3..3a036e3 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
@@ -13,6 +13,9 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+INCLUDE PERFETTO MODULE slices.with_context;
+INCLUDE PERFETTO MODULE android.frames.timeline_maxsdk28;
+
 -- Parses the slice name to fetch `frame_id` from `slice` table.
 -- Use with caution. Slice names are a flaky source of ids and the resulting
 -- table might require some further operations.
@@ -23,14 +26,20 @@
     -- `slice.id` of the frame slice.
     id INT,
     -- Parsed frame id.
-    frame_id INT
+    frame_id INT,
+    -- Utid.
+    utid INT,
+    -- Upid.
+    upid INT
 ) AS
 WITH all_found AS (
     SELECT
         id,
-        cast_int!(STR_SPLIT(name, ' ', 1)) AS frame_id
-    FROM slice
-    WHERE name GLOB $glob_str
+        cast_int!(STR_SPLIT(name, ' ', 1)) AS frame_id,
+        utid,
+        upid
+    FROM thread_slice
+    WHERE name GLOB $glob_str AND depth = 0
 )
 SELECT *
 FROM all_found
@@ -44,14 +53,18 @@
     -- Frame id
     frame_id INT,
     -- Utid of the UI thread
-    ui_thread_utid INT
+    ui_thread_utid INT,
+    -- Upid of application process
+    upid INT
 ) AS
 SELECT
-    c.*,
-    thread_track.utid AS ui_thread_utid
-FROM _get_frame_table_with_id('Choreographer#doFrame*') c
-JOIN slice USING (id)
-JOIN thread_track ON (thread_track.id = slice.track_id);
+    id,
+    frame_id,
+    utid AS ui_thread_utid,
+    upid
+-- Some OEMs have customized `doFrame` to add more information, but we've only
+-- observed it added after the frame ID (b/303823815).
+FROM _get_frame_table_with_id('Choreographer#doFrame*');
 
 -- All of the `DrawFrame` slices with their frame id and render thread.
 -- There might be multiple DrawFrames slices for a single vsync (frame id).
@@ -63,14 +76,16 @@
     -- Frame id
     frame_id INT,
     -- Utid of the render thread
-    render_thread_utid INT
+    render_thread_utid INT,
+    -- Upid of application process
+    upid INT
 ) AS
 SELECT
-    d.*,
-    thread_track.utid AS render_thread_utid
-FROM _get_frame_table_with_id('DrawFrame*') d
-JOIN slice USING (id)
-JOIN thread_track ON (thread_track.id = slice.track_id);
+    id,
+    frame_id,
+    utid AS render_thread_utid,
+    upid
+FROM _get_frame_table_with_id('DrawFrame*');
 
 -- `actual_frame_timeline_slice` returns the same slice on different tracks.
 -- We are getting the first slice with one frame id.
@@ -101,10 +116,11 @@
     frame_id INT,
     -- Timestamp of the frame. Start of the frame as defined by the start of
     -- "Choreographer#doFrame" slice and the same as the start of the frame in
-    -- `actual_frame_timeline_slice .
+    -- `actual_frame_timeline_slice if present.
     ts INT,
     -- Duration of the frame, as defined by the duration of the corresponding
-    -- `actual_frame_timeline_slice` duration.
+    -- `actual_frame_timeline_slice` or, if not present the time between the
+    -- `ts` and the end of the final `DrawFrame`.
     dur INT,
     -- `slice.id` of "Choreographer#doFrame" slice.
     do_frame_id INT,
@@ -119,21 +135,59 @@
     -- `utid` of the UI thread.
     ui_thread_utid INT
 ) AS
+WITH fallback AS MATERIALIZED (
+    SELECT
+        frame_id,
+        do_frame_slice.ts AS ts,
+        MAX(draw_frame_slice.ts + draw_frame_slice.dur) - do_frame_slice.ts AS dur
+    FROM android_frames_choreographer_do_frame do_frame
+    JOIN android_frames_draw_frame draw_frame USING (frame_id, upid)
+    JOIN slice do_frame_slice ON (do_frame.id = do_frame_slice.id)
+    JOIN slice draw_frame_slice ON (draw_frame.id = draw_frame_slice.id)
+GROUP BY 1
+),
+frames_sdk_after_28 AS (
+SELECT
+    frame_id,
+    COALESCE(act.ts, fallback.ts) AS ts,
+    COALESCE(act.dur, fallback.dur) AS dur,
+    do_frame.id AS do_frame_id,
+    draw_frame.id AS draw_frame_id,
+    draw_frame.render_thread_utid,
+    do_frame.ui_thread_utid,
+    "after_28" AS sdk,
+    act.id AS actual_frame_timeline_id,
+    exp.id AS expected_frame_timeline_id
+FROM android_frames_choreographer_do_frame do_frame
+JOIN android_frames_draw_frame draw_frame USING (frame_id, upid)
+JOIN fallback USING (frame_id)
+LEFT JOIN _distinct_from_actual_timeline_slice act USING (frame_id)
+LEFT JOIN _distinct_from_expected_timeline_slice exp USING (frame_id)
+ORDER BY frame_id
+),
+all_frames AS (
+    SELECT * FROM frames_sdk_after_28
+    UNION
+    SELECT
+        *,
+        NULL AS actual_frame_timeline_id,
+        NULL AS expected_frame_timeline_id
+    FROM _frames_maxsdk_28
+)
 SELECT
     frame_id,
     ts,
     dur,
-    do_frame.id AS do_frame_id,
-    draw_frame.id AS draw_frame_id,
-    act.id AS actual_frame_timeline_id,
-    exp.id AS expected_frame_timeline_id,
-    draw_frame.render_thread_utid,
-    do_frame.ui_thread_utid
-FROM android_frames_choreographer_do_frame do_frame
-JOIN android_frames_draw_frame draw_frame USING (frame_id)
-JOIN _distinct_from_actual_timeline_slice act USING (frame_id)
-JOIN _distinct_from_expected_timeline_slice exp USING (frame_id)
-ORDER BY frame_id;
+    do_frame_id,
+    draw_frame_id,
+    actual_frame_timeline_id,
+    expected_frame_timeline_id,
+    render_thread_utid,
+    ui_thread_utid
+FROM all_frames
+WHERE sdk = IIF(
+    (SELECT COUNT(1) FROM actual_frame_timeline_slice) > 0,
+    "after_28", "maxsdk28");
 
 -- Returns first frame after the provided timestamp. The returning table has at
 -- most one row.
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline_maxsdk28.sql b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline_maxsdk28.sql
new file mode 100644
index 0000000..b8fad07
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline_maxsdk28.sql
@@ -0,0 +1,76 @@
+--
+-- 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.with_context;
+
+-- All slices related to one frame for max SDK 28. Aggregates
+-- "Choreographer#doFrame" and "DrawFrame". Tries to guess the `ts` and `dur`
+-- of the frame by first guessing which "DrawFrame" slices are related to which
+-- "Choreographer#doSlice".
+CREATE PERFETTO TABLE _frames_maxsdk_28(
+    -- Frame id. Created manually starting from 0.
+    frame_id INT,
+    -- Timestamp of the frame. Start of "Choreographer#doFrame" slice.
+    ts INT,
+    -- Duration of the frame, defined as the duration until the last
+    -- "DrawFrame" of this frame finishes.
+    dur INT,
+    -- `slice.id` of "Choreographer#doFrame" slice.
+    do_frame_id INT,
+    -- `slice.id` of "DrawFrame" slice. Fetched as one of the "DrawFrame"
+    -- slices that happen for the same process as "Choreographer#doFrame" slice
+    -- and start after it started and before the next "doFrame" started.
+    draw_frame_id INT,
+    -- `utid` of the render thread.
+    render_thread_utid INT,
+    -- `utid` of the UI thread.
+    ui_thread_utid INT,
+    -- "maxsdk28"
+    sdk STRING
+) AS
+WITH do_frames AS (
+    SELECT
+        id,
+        ts,
+        LEAD(ts, 1, TRACE_END()) OVER (PARTITION BY upid ORDER BY ts) AS next_do_frame,
+        utid,
+        upid
+    FROM thread_slice
+    WHERE name = 'Choreographer#doFrame' AND is_main_thread = 1
+    ORDER BY ts
+),
+draw_frames AS (
+    SELECT
+        id,
+        ts,
+        dur,
+        ts + dur AS ts_end,
+        utid,
+        upid
+    FROM thread_slice
+    WHERE name = 'DrawFrame'
+)
+SELECT
+  ROW_NUMBER() OVER () AS frame_id,
+  do.ts,
+  MAX(draw.ts_end) OVER (PARTITION BY do.id) - do.ts AS dur,
+  do.id AS do_frame_id,
+  draw.id AS draw_frame_id,
+  draw.utid AS render_thread_utid,
+  do.utid AS ui_thread_utid,
+  "maxsdk28" AS sdk
+FROM do_frames do
+JOIN draw_frames draw ON (do.upid = draw.upid AND draw.ts >= do.ts AND draw.ts < next_do_frame)
+ORDER BY do.ts;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql b/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
index 86c099b..a50b033 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
@@ -70,8 +70,8 @@
     )
     OR
     (
-      -- isolated processes can only be matched based on the name prefix
+      -- isolated processes can only be matched based on the name
       process.android_appid >= 90000 AND process.android_appid < 100000
-      AND STR_SPLIT(process.name, ':', 0) GLOB plist.package_name || '*'
+      AND STR_SPLIT(process.name, ':', 0) = plist.package_name
     )
   );
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
index 8206b4a..c495f08 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/BUILD.gn
@@ -21,5 +21,6 @@
     "startups_maxsdk28.sql",
     "startups_minsdk29.sql",
     "startups_minsdk33.sql",
+    "time_to_display.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
index 0a7fab2..3faf091 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
@@ -13,17 +13,57 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+INCLUDE PERFETTO MODULE slices.with_context;
 INCLUDE PERFETTO MODULE android.startup.startup_events;
+INCLUDE PERFETTO MODULE android.frames.timeline;
 
 CREATE PERFETTO TABLE _startups_maxsdk28 AS
+-- Warm and cold starts only are based on the launching slice
+WITH warm_and_cold AS (
+  SELECT
+    le.ts,
+    le.ts_end AS ts_end,
+    package_name AS package,
+    NULL AS startup_type
+  FROM _startup_events le
+),
+-- Hot starts don’t have a launching slice so we use activityResume as a
+-- proxy.
+--
+-- Note that this implementation will also count warm and cold starts but
+-- we will remove those below.
+maybe_hot AS (
+  SELECT
+    sl.ts,
+    rs.ts + rs.dur AS ts_end,
+    -- We use the process name as the package as we have no better option.
+    process_name AS package,
+    "hot" AS startup_type
+  FROM thread_slice sl
+  JOIN android_first_frame_after(sl.ts) rs
+  WHERE name = 'activityResume'
+  -- Remove any launches here where the activityResume slices happens during
+  -- a warm/cold startup.
+  AND NOT EXISTS (
+    SELECT 1
+    FROM warm_and_cold wac
+    WHERE sl.ts BETWEEN wac.ts AND wac.ts_end
+    LIMIT 1)
+),
+cold_warm_hot AS (
+  SELECT * FROM warm_and_cold
+  UNION ALL
+  SELECT * FROM maybe_hot
+
+)
 SELECT
-  "maxsdk28" as sdk,
+  "maxsdk28" AS sdk,
   ROW_NUMBER() OVER(ORDER BY ts) AS startup_id,
-  le.ts,
-  le.ts_end AS ts_end,
-  le.ts_end - le.ts AS dur,
-  package_name AS package,
-  NULL AS startup_type
-FROM _startup_events le
-ORDER BY ts;
+  ts,
+  ts_end,
+  ts_end - ts AS dur,
+  package,
+  startup_type
+FROM cold_warm_hot;
+
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/time_to_display.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/time_to_display.sql
new file mode 100644
index 0000000..7264bb7
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/time_to_display.sql
@@ -0,0 +1,164 @@
+--
+-- 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 android.startup.startups;
+INCLUDE PERFETTO MODULE android.frames.timeline;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+CREATE PERFETTO VIEW _startups_with_upid AS
+WITH joined_with_processes AS (
+  SELECT
+    s.*,
+    p.upid
+  FROM android_startups s
+  LEFT JOIN android_startup_processes p USING (startup_id)
+),
+fallback AS (
+  SELECT
+    s.*,
+    upid FROM android_startups s
+  JOIN process p ON (p.name glob s.package)
+)
+  SELECT
+    j.startup_id,
+    j.ts,
+    j.ts_end,
+    j.dur,
+    j.package,
+    j.startup_type,
+    COALESCE(j.upid, f.upid) AS upid
+  FROM joined_with_processes j
+  LEFT JOIN fallback f ON (j.upid IS NULL AND j.startup_id = f.startup_id);
+
+-- Get Time To Initial Display of the startup calculated as time between the
+-- startup started and the first frame that was started by Choreographer on the
+-- UI thread of the startup finished drawing.
+-- TTID (https://developer.android.com/topic/performance/vitals/launch-time#time-initial)
+-- Googlers: see go/android-performance-metrics-glossary for details.
+CREATE PERFETTO TABLE _ttid AS
+WITH frames_with_upid AS (
+  SELECT
+  f.*,
+  upid
+  FROM android_frames f
+  JOIN thread t ON (f.ui_thread_utid = t.utid)
+),
+  -- First `DrawFrame` on Render Thread after the startup.
+first_frame_for_startup AS (
+  SELECT
+    startup_id,
+    frame_id,
+    s.ts AS startup_ts,
+    draw_frame_id,
+    s.upid
+  FROM _startups_with_upid s
+  JOIN frames_with_upid f ON (s.upid = f.upid AND s.ts <= f.ts)
+  GROUP BY startup_id
+  ORDER BY startup_id, f.ts)
+SELECT
+  startup_id,
+  frame_id,
+  draw_frame_id,
+  ts + dur - startup_ts AS ttid,
+  upid
+FROM first_frame_for_startup
+JOIN slice ON (slice.id = draw_frame_id);
+
+-- Get Time To Full Display of the startup calculated as time between the
+-- startup started and the first frame that was started by Choreographer after
+-- or containing the `reportFullyDrawn()` slice on the UI thread of the startup
+-- finished drawing.
+-- TTFD (https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD)
+-- Googlers: see go/android-performance-metrics-glossary for details.
+CREATE PERFETTO TABLE _ttfd AS
+-- First `reportFullyDrawn` slice for each startup.
+WITH first_report_fully_drawn_for_startup AS (
+  SELECT
+  startup_id,
+  s.ts AS startup_ts,
+  t.ts AS report_fully_drawn_ts,
+  t.utid,
+  s.upid
+FROM _startups_with_upid s
+JOIN thread_slice t ON (s.upid = t.upid AND t.ts >= s.ts)
+WHERE name GLOB "reportFullyDrawn*" AND t.is_main_thread = 1
+GROUP BY startup_id
+ORDER BY startup_id, t.ts),
+-- After the first `reportFullyDrawn` find the first `Choreographer#DoFrame` on
+-- the UI thread and it's first `DrawFrame` on Render Thread.
+first_frame_after_report_for_startup AS (
+  SELECT
+    startup_id,
+    frame_id,
+    startup_ts,
+    draw_frame_id,
+    s.upid
+  FROM first_report_fully_drawn_for_startup s
+  JOIN android_frames f ON (
+    s.utid = f.ui_thread_utid
+    -- We are looking for the first DrawFrame that was started by the first
+    -- "Choreographer#DoFrame" on UI thread after or containing
+    -- reportFullyDrawn. In Android UIs, it's common to have UI code happen
+    -- either before a frame, or during it, and generally non-trivial amounts
+    -- of "update UI model" code doesn't try to differentiate these. We account
+    -- for both of these by looking for the first UI slice that ends after the
+    -- "reportFullyDrawnSlice" begins.
+    AND report_fully_drawn_ts < (f.ts + f.dur))
+  GROUP BY startup_id
+  ORDER BY startup_id, f.ts
+)
+-- Get TTFD as the difference between the start of the startup and the end of
+-- `DrawFrame` slice we previously found.
+SELECT
+  startup_id,
+  frame_id,
+  draw_frame_id,
+  ts + dur - startup_ts AS ttfd,
+  upid
+FROM first_frame_after_report_for_startup
+JOIN slice ON (slice.id = draw_frame_id);
+
+-- Startup metric defintions, which focus on the observable time range:
+-- TTID - Time To Initial Display
+-- * https://developer.android.com/topic/performance/vitals/launch-time#time-initial
+-- * end of first RenderThread.DrawFrame - bindApplication
+-- TTFD - Time To Full Display
+-- * https://developer.android.com/topic/performance/vitals/launch-time#retrieve-TTFD
+-- * end of next RT.DrawFrame, after reportFullyDrawn called - bindApplication
+-- Googlers: see go/android-performance-metrics-glossary for details.
+CREATE PERFETTO TABLE android_startup_time_to_display(
+  -- Startup id.
+  startup_id INT,
+  -- Time to initial display (TTID)
+  time_to_inital_display INT,
+  -- Time to full display (TTFD)
+  time_to_full_display INT,
+  -- `android_frames.frame_id` of frame for initial display
+  ttid_frame_id INT,
+  -- `android_frames.frame_id` of frame for full display
+  ttfd_frame_id INT,
+  -- `process.upid` of the startup
+  upid INT
+) AS
+SELECT
+  startup_id,
+  ttid AS time_to_inital_display,
+  ttfd AS time_to_full_display,
+  _ttid.frame_id AS ttid_frame_id,
+  _ttfd.frame_id AS ttfd_frame_id,
+  _ttid.upid
+FROM android_startups
+LEFT JOIN _ttid USING (startup_id)
+LEFT JOIN _ttfd USING (startup_id);
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
index f6e29f9..fcc8933 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
@@ -2,6 +2,8 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
+INCLUDE PERFETTO MODULE chrome.scroll_jank.utils;
+
 -- Defines slices for all of the individual scrolls in a trace based on the
 -- LatencyInfo-based scroll definition.
 --
@@ -19,36 +21,39 @@
   ts INT,
   -- The duration of the scroll.
   dur INT,
-  -- The earliest timestamp of the InputLatency::GestureScrollBegin for the
+  -- The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_BEGIN type for the
   -- corresponding scroll id.
   gesture_scroll_begin_ts INT,
-  -- The earliest timestamp of the InputLatency::GestureScrollEnd for the
+  -- The earliest timestamp of the EventLatency slice of the GESTURE_SCROLL_END type /
+  -- the latest timestamp of the EventLatency slice of the GESTURE_SCROLL_UPDATE type for the
   -- corresponding scroll id.
   gesture_scroll_end_ts INT
 ) AS
 WITH all_scrolls AS (
   SELECT
-    name,
-    ts,
-    dur,
-    extract_arg(arg_set_id, 'chrome_latency_info.gesture_scroll_id') AS scroll_id
-  FROM slice
-  WHERE name GLOB 'InputLatency::GestureScroll*'
-  AND extract_arg(arg_set_id, 'chrome_latency_info.gesture_scroll_id') IS NOT NULL
+    args.string_value AS name,
+    S.ts AS ts,
+    S.dur AS dur,
+    chrome_get_most_recent_scroll_begin_id(S.ts) AS scroll_id
+  FROM slice AS S JOIN args USING(arg_set_id)
+  WHERE name="EventLatency"
+  AND args.string_value GLOB "*GESTURE_SCROLL*"
 ),
 scroll_starts AS (
   SELECT
     scroll_id,
     MIN(ts) AS gesture_scroll_begin_ts
   FROM all_scrolls
-  WHERE name = 'InputLatency::GestureScrollBegin'
+  WHERE name = "GESTURE_SCROLL_BEGIN"
   GROUP BY scroll_id
-), scroll_ends AS (
+),
+scroll_ends AS (
   SELECT
     scroll_id,
-    MIN(ts) AS gesture_scroll_end_ts
+    MAX(ts) AS gesture_scroll_end_ts
   FROM all_scrolls
-  WHERE name = 'InputLatency::GestureScrollEnd'
+  WHERE name GLOB "*GESTURE_SCROLL_UPDATE"
+    OR name = "GESTURE_SCROLL_END"
   GROUP BY scroll_id
 )
 SELECT
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_offsets.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_offsets.sql
index 8df0937..f1bfd37 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_offsets.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_offsets.sql
@@ -29,189 +29,104 @@
 -- various stages of input processing, and are unified by a single
 -- scroll_update_id value, recorded as scroll_deltas.trace_id in each event.
 
+INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
 INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_v3;
 
--- Non-coalesced scroll update events and their timestamps.
-CREATE PERFETTO VIEW _non_coalesced_scrolls AS
-SELECT
-  scroll_update_id,
-  ts
-FROM chrome_gesture_scroll_updates
-WHERE is_coalesced = False;
-
 -- All (coalesced and non-coalesced) vertical scrolling deltas and their
 -- associated scroll ids. Delta values are recorded after being scaled to the
 -- device's screen size in the TranslateAndScaleWebInputEvent trace event. In
 -- this trace event, the deltas recorded represent the true (read "original")
 -- values that the Browser receives from Android, and the only processing is
 -- scaling and translation.
-CREATE PERFETTO TABLE _scroll_deltas AS
+CREATE PERFETTO TABLE _translate_and_scale_scroll_deltas AS
 SELECT
   EXTRACT_ARG(arg_set_id, 'scroll_deltas.trace_id') AS scroll_update_id,
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.original_delta_y') AS delta_y,
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.original_delta_y') IS NOT NULL AS is_coalesced
+  EXTRACT_ARG(arg_set_id, 'scroll_deltas.original_delta_y') AS delta_y
 FROM slice
-WHERE name = "TranslateAndScaleWebInputEvent";
+WHERE slice.name = 'TranslateAndScaleWebInputEvent';
 
--- Associate the raw (original) deltas (_scroll_deltas) with the
--- corresponding non-coalesced scroll updates
--- (_non_coalesced_scroll_updates) to get the timestamp of the event
--- those deltas. This allows for ordering delta recordings to track them over
--- time.
-CREATE PERFETTO VIEW _non_coalesced_deltas AS
+-- Associate the gesture scroll update OS timestamp with the delta.
+CREATE PERFETTO TABLE _scroll_deltas_with_timestamp AS
 SELECT
-  scroll_update_id,
-  ts,
-  delta_y
-FROM _non_coalesced_scrolls
-INNER JOIN _scroll_deltas
-  USING (scroll_update_id);
+  slice.ts AS input_ts,
+  data.scroll_update_id,
+  data.delta_y
+FROM _translate_and_scale_scroll_deltas data
+  JOIN slice ON slice.name = 'EventLatency'
+    AND data.scroll_update_id = EXTRACT_ARG(arg_set_id,
+        'event_latency.event_latency_id');
 
--- Selecting information scroll update events that have been coalesced,
--- including timestamp and the specific event (scroll update id) it was
--- coalesced into. Recordings of deltas will need to be associated with the
--- timestamp of the scroll update they were coalesced into.
-CREATE PERFETTO TABLE _scroll_update_coalesce_info AS
+-- Associate the scroll update/delta with the correct scroll.
+CREATE PERFETTO TABLE _scroll_deltas_with_scroll_id AS
 SELECT
-  ts,
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.coalesced_to_trace_id') AS coalesced_to_scroll_update_id,
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.trace_id') AS scroll_update_id
-FROM slice
-WHERE name = "WebCoalescedInputEvent::CoalesceWith" AND
-  coalesced_to_scroll_update_id IS NOT NULL;
+  scrolls.id AS scroll_id,
+  deltas.input_ts,
+  deltas.scroll_update_id,
+  deltas.delta_y
+FROM _scroll_deltas_with_timestamp deltas
+  LEFT JOIN chrome_scrolls scrolls
+    ON deltas.input_ts >= scrolls.ts
+      AND deltas.input_ts <= scrolls.ts + scrolls.dur;
 
--- Associate the raw (original) deltas (_scroll_deltas) with the
--- corresponding coalesced scroll updates (_scroll_update_coalesce_info)
--- to get the timestamp of the event those deltas were coalesced into. This
--- allows us to get the scaled coordinates for all of the input events
--- (original input coordinates can't be used due to scaling).
-CREATE PERFETTO VIEW _coalesced_deltas AS
+-- Associate the presentation timestamp/deltas with the user deltas.
+CREATE PERFETTO TABLE _scroll_deltas_with_delays AS
 SELECT
-  _scroll_update_coalesce_info.coalesced_to_scroll_update_id AS scroll_update_id,
-  ts,
-  _scroll_deltas.delta_y AS delta_y,
-  TRUE AS is_coalesced
-FROM _scroll_update_coalesce_info
-LEFT JOIN _scroll_deltas
-  USING (scroll_update_id);
-
--- All of the presented frame scroll update ids.
-CREATE PERFETTO VIEW chrome_deltas_presented_frame_scroll_update_ids(
-  -- A scroll update id that was included in the presented frame.
-  -- There may be zero, one, or more.
-  scroll_update_id INT,
-  -- Slice id
-  id INT
-) AS
-SELECT
-  args.int_value AS scroll_update_id,
-  slice.id
-FROM args
-LEFT JOIN slice
-  USING (arg_set_id)
-WHERE slice.name = 'PresentedFrameInformation'
-AND args.flat_key GLOB 'scroll_deltas.trace_ids_in_gpu_frame*';;
-
--- When every GestureScrollUpdate event is processed, the offset set by the
--- compositor is recorded. This offset is scaled to the device screen size, and
--- can be used to calculate deltas.
-CREATE PERFETTO VIEW _presented_frame_offsets AS
-SELECT
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.trace_id') AS scroll_update_id,
-  EXTRACT_ARG(arg_set_id, 'scroll_deltas.visual_offset_y') AS visual_offset_y
-FROM slice
-WHERE name = 'InputHandlerProxy::HandleGestureScrollUpdate_Result';
+  deltas.scroll_id,
+  delay.total_delta,
+  delay.scroll_update_id,
+  delay.presentation_timestamp AS presentation_timestamp,
+  deltas.input_ts,
+  deltas.delta_y
+FROM _scroll_deltas_with_scroll_id AS deltas
+  LEFT JOIN chrome_frame_info_with_delay AS delay USING(scroll_update_id);
 
 -- The raw coordinates and pixel offsets for all input events which were part of
--- a scroll. This includes input events that were converted to scroll events
--- which were presented (_non_coalesced_scrolls) and scroll events which
--- were coalesced (_coalesced_deltas).
+-- a scroll.
 CREATE PERFETTO TABLE chrome_scroll_input_offsets(
   -- Trace id associated with the scroll.
+  scroll_id INT,
+  -- Trace id associated with the scroll.
   scroll_update_id INT,
   -- Timestamp the of the scroll input event.
   ts INT,
-  -- The delta in raw coordinates between this scroll update event and the previous.
+  -- The delta in raw coordinates between this scroll update event and the
+  -- previous.
   delta_y INT,
-  -- The pixel offset of this scroll update event compared to the previous one.
-  offset_y INT
+  -- The pixel offset of this scroll update event compared to the initial one.
+  relative_offset_y INT
 ) AS
--- First collect all coalesced and non-coalesced deltas so that the offsets
--- can be calculated from them in order of timestamp.
-WITH all_deltas AS (
-  SELECT
-    scroll_update_id,
-    ts,
-    delta_y
-  FROM _non_coalesced_deltas
-  WHERE delta_y IS NOT NULL
-  UNION
-  SELECT
-    scroll_update_id,
-    ts,
-    delta_y
-  FROM _coalesced_deltas
-  WHERE delta_y IS NOT NULL
-  ORDER BY scroll_update_id, ts)
 SELECT
+  scroll_id,
   scroll_update_id,
-  ts,
+  input_ts AS ts,
   delta_y,
-  SUM(IFNULL(delta_y, 0)) OVER (
-    ORDER BY scroll_update_id, ts
-    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS offset_y
-FROM all_deltas;
-
--- Calculate the total visual offset for all presented frames (non-coalesced
--- scroll updates) that have raw deltas recorded. These visual offsets
--- correspond with the inverse of the deltas for the presented frame.
-CREATE PERFETTO VIEW _preprocessed_presented_frame_offsets AS
-SELECT
-  chrome_full_frame_view.scroll_update_id,
-  chrome_full_frame_view.presentation_timestamp AS ts,
-  chrome_deltas_presented_frame_scroll_update_ids.id,
-  _presented_frame_offsets.visual_offset_y -
-    LAG(_presented_frame_offsets.visual_offset_y)
-    OVER (ORDER BY chrome_full_frame_view.presentation_timestamp)
-      AS presented_frame_visual_offset_y
-FROM chrome_full_frame_view
-LEFT JOIN _scroll_deltas
-  USING (scroll_update_id)
-LEFT JOIN chrome_deltas_presented_frame_scroll_update_ids
-  USING (scroll_update_id)
-LEFT JOIN _presented_frame_offsets
-  USING (scroll_update_id)
-WHERE _scroll_deltas.delta_y IS NOT NULL;
+  SUM(IFNULL(delta_y, 0)) OVER ( PARTITION BY scroll_id
+    ORDER BY scroll_update_id, input_ts
+    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS relative_offset_y
+FROM _scroll_deltas_with_delays;
 
 -- The scrolling offsets for the actual (applied) scroll events. These are not
 -- necessarily inclusive of all user scroll events, rather those scroll events
 -- that are actually processed.
 CREATE PERFETTO TABLE chrome_presented_scroll_offsets(
-  -- Trace Id associated with the scroll.
+  -- Trace id associated with the scroll.
+  scroll_id INT,
+  -- Trace id associated with the scroll update event.
   scroll_update_id INT,
   -- Presentation timestamp.
   ts INT,
-  -- The delta in coordinates as processed by Chrome between this scroll update
-  -- event and the previous.
+  -- The delta in raw coordinates between this scroll update event and the
+  -- previous.
   delta_y INT,
-  -- The pixel offset of this scroll update (the presented frame) compared to
-  -- the previous one.
-  offset_y INT
+  -- The pixel offset of this scroll update event compared to the initial one.
+  relative_offset_y INT
 ) AS
-WITH all_deltas AS (
-  SELECT
-    scroll_update_id,
-    id,
-    MAX(ts) AS ts,
-    SUM(presented_frame_visual_offset_y) * -1 AS delta_y
-  FROM _preprocessed_presented_frame_offsets
-  GROUP BY id
-  ORDER BY ts)
 SELECT
+  scroll_id,
   scroll_update_id,
-  ts,
-  delta_y,
-  SUM(IFNULL(delta_y, 0)) OVER (
-    ORDER BY scroll_update_id, ts
-    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS offset_y
-FROM all_deltas;
+  presentation_timestamp AS ts,
+  total_delta AS delta_y,
+  SUM(IFNULL(total_delta, 0)) OVER ( PARTITION BY scroll_id
+    ORDER BY scroll_update_id, presentation_timestamp
+    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS relative_offset_y
+FROM _scroll_deltas_with_delays;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
index 74141e6..b7b6bad 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/utils.sql
@@ -103,3 +103,18 @@
 WHERE
   category GLOB "*scheduler.long_tasks*"
   AND name = $name;
+
+-- Extracts scroll id for the EventLatency slice at `ts`.
+CREATE PERFETTO FUNCTION chrome_get_most_recent_scroll_begin_id(
+  -- Timestamp of the EventLatency slice to get the scroll id for.
+  ts INT)
+-- The event_latency_id of the EventLatency slice with the type
+-- GESTURE_SCROLL_BEGIN that is the closest to `ts`.
+RETURNS INT AS
+SELECT EXTRACT_ARG(arg_set_id, "event_latency.event_latency_id")
+FROM slice
+WHERE name="EventLatency"
+AND EXTRACT_ARG(arg_set_id, "event_latency.event_type") = "GESTURE_SCROLL_BEGIN"
+AND ts<=$ts
+ORDER BY ts DESC
+LIMIT 1;
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS b/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
index a2788c4..0a16b3f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
+++ b/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
@@ -6,4 +6,3 @@
 
 # For emergency reviews
 primiano@google.com
-hjd@google.com
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 28edf7f..f0cfae1 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
@@ -18,20 +18,21 @@
 -- Excluding following types from the graph as they share objects' ownership
 -- with their real (more interesting) owners and will mask their idom to be the
 -- "super root".
-CREATE PERFETTO TABLE _excluded_type_ids AS
-WITH RECURSIVE class_visitor(type_id) AS (
-  SELECT id AS type_id
-  FROM heap_graph_class
-  WHERE name IN (
-    'java.lang.ref.PhantomReference',
-    'java.lang.ref.FinalizerReference'
-  )
-  UNION ALL
-  SELECT child.id AS type_id
-  FROM heap_graph_class child
-  JOIN class_visitor parent ON parent.type_id = child.superclass_id
-)
-SELECT * FROM class_visitor;
+CREATE PERFETTO TABLE _ref_type_ids AS
+SELECT id AS type_id FROM heap_graph_class
+WHERE kind IN (
+  'KIND_FINALIZER_REFERENCE',
+  'KIND_PHANTOM_REFERENCE',
+  'KIND_SOFT_REFERENCE',
+  'KIND_WEAK_REFERENCE');
+
+CREATE PERFETTO TABLE _excluded_refs AS
+SELECT ref.id
+  FROM _ref_type_ids
+  JOIN heap_graph_object robj USING (type_id)
+  JOIN heap_graph_reference ref USING (reference_set_id)
+WHERE ref.field_name = 'java.lang.ref.Reference.referent'
+ORDER BY ref.id;
 
 -- The assigned id of the "super root".
 -- Since a Java heap graph is a "forest" structure, we need to add a imaginary
@@ -48,7 +49,8 @@
   ref.owned_id AS dest_node_id
 FROM heap_graph_reference ref
 JOIN heap_graph_object source_node ON ref.owner_id = source_node.id
-WHERE source_node.reachable AND source_node.type_id NOT IN _excluded_type_ids
+WHERE source_node.reachable
+  AND ref.id NOT IN _excluded_refs
   AND ref.owned_id IS NOT NULL
 UNION ALL
 SELECT
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/states.sql b/src/trace_processor/perfetto_sql/stdlib/sched/states.sql
index 37f759a..f6ce66a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/states.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/states.sql
@@ -46,6 +46,12 @@
 WHEN 'W' THEN 'Waking'
 WHEN 'P' THEN 'Parked'
 WHEN 'N' THEN 'No Load'
+-- ETW SPECIFIC STATES
+WHEN 'Stand By' THEN 'Stand By'
+WHEN 'Initialized' THEN 'Initialized'
+WHEN 'Waiting' THEN 'Waiting'
+WHEN 'Transition' THEN 'Transition'
+WHEN 'Deferred Ready' THEN 'Deferred Ready'
 ELSE $short_name
 END;
 
@@ -69,4 +75,4 @@
     WHEN 0 THEN ' (non-IO)'
     ELSE ''
   END
-);
\ No newline at end of file
+);
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql b/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
index 6e9ba0d..9430bd9 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
@@ -36,8 +36,10 @@
   thread_name STRING,
   -- Alias for `thread.utid`.
   utid INT,
-  -- Alias for `thread.tid`
+  -- Alias for `thread.tid`.
   tid INT,
+  -- Alias for `thread.is_main_thread`.
+  is_main_thread BOOL,
   -- Alias for `process.name`.
   process_name STRING,
   -- Alias for `process.upid`.
@@ -67,6 +69,7 @@
   thread.name AS thread_name,
   thread.utid,
   thread.tid,
+  thread.is_main_thread,
   process.name AS process_name,
   process.upid,
   process.pid,
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
new file mode 100644
index 0000000..3f664bd
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
@@ -0,0 +1,24 @@
+# Copyright (C) 2023 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("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("summary") {
+  sources = [
+    "processes.sql",
+    "slices.sql",
+    "threads.sql",
+    "tracks.sql",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql
new file mode 100644
index 0000000..18aa202
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/processes.sql
@@ -0,0 +1,93 @@
+--
+-- 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 viz.summary.slices;
+INCLUDE PERFETTO MODULE viz.summary.threads;
+
+CREATE PERFETTO TABLE _process_track_summary AS
+SELECT upid, SUM(cnt) AS slice_count
+FROM process_track
+JOIN _slice_track_summary USING (id)
+GROUP BY upid;
+
+CREATE PERFETTO TABLE _heap_profile_allocation_summary AS
+SELECT upid, COUNT() AS allocation_count
+FROM heap_profile_allocation
+GROUP BY upid;
+
+CREATE PERFETTO TABLE _heap_profile_graph_summary AS
+SELECT upid, COUNT() AS graph_object_count
+FROM heap_graph_object;
+
+CREATE PERFETTO TABLE _thread_process_grouped_summary AS
+SELECT
+  upid,
+  MAX(max_running_dur) AS max_running_dur,
+  SUM(sum_running_dur) AS sum_running_dur,
+  SUM(running_count) AS running_count,
+  SUM(slice_count) AS slice_count,
+  SUM(perf_sample_count) AS perf_sample_count
+FROM _thread_available_info_summary
+JOIN thread USING (utid)
+WHERE upid IS NOT NULL
+GROUP BY upid;
+
+CREATE PERFETTO TABLE _process_available_info_summary AS
+WITH r AS (
+  SELECT
+    upid,
+    t_summary.upid as summary_upid,
+    t_summary.max_running_dur AS max_running_dur,
+    t_summary.sum_running_dur,
+    t_summary.running_count,
+    t_summary.slice_count AS thread_slice_count,
+    t_summary.perf_sample_count AS perf_sample_count,
+    (
+      SELECT slice_count
+      FROM _process_track_summary
+      WHERE upid = p.upid
+    ) AS process_slice_count,
+    (
+      SELECT allocation_count
+      FROM _heap_profile_allocation_summary
+      WHERE upid = p.upid
+    ) AS allocation_count,
+    (
+      SELECT graph_object_count
+      FROM _heap_profile_graph_summary
+      WHERE upid = p.upid
+    ) AS graph_object_count
+  FROM process p
+  LEFT JOIN _thread_process_grouped_summary t_summary USING (upid)
+)
+SELECT
+  upid,
+  IFNULL(max_running_dur, 0) AS max_running_dur,
+  IFNULL(sum_running_dur, 0) AS sum_running_dur,
+  IFNULL(running_count, 0) AS running_count,
+  IFNULL(thread_slice_count, 0) AS thread_slice_count,
+  IFNULL(perf_sample_count, 0) AS perf_sample_count,
+  IFNULL(process_slice_count, 0) AS process_slice_count,
+  IFNULL(allocation_count, 0) AS allocation_count,
+  IFNULL(graph_object_count, 0) AS graph_object_count
+FROM r
+WHERE
+  NOT(
+    r.summary_upid IS NULL
+    AND process_slice_count IS NULL
+    AND allocation_count IS NULL
+    AND graph_object_count IS NULL
+  )
+  OR upid IN (SELECT upid FROM process_counter_track);
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/slices.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/slices.sql
new file mode 100644
index 0000000..c698859
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/slices.sql
@@ -0,0 +1,24 @@
+--
+-- 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.
+
+CREATE PERFETTO TABLE _slice_track_summary AS
+SELECT
+  track_id as id,
+  COUNT() AS cnt,
+  MIN(dur) AS min_dur,
+  MAX(dur) AS max_dur,
+  MAX(depth) AS max_depth
+FROM slice
+GROUP BY track_id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql
new file mode 100644
index 0000000..ed74acf
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql
@@ -0,0 +1,77 @@
+--
+-- 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 viz.summary.slices;
+
+CREATE PERFETTO TABLE _sched_summary AS
+SELECT
+  utid,
+  MAX(dur) AS max_running_dur,
+  SUM(dur) AS sum_running_dur,
+  COUNT() AS running_count
+FROM sched
+WHERE utid != 0 AND dur != -1
+GROUP BY utid;
+
+CREATE PERFETTO TABLE _thread_track_summary AS
+SELECT utid, SUM(cnt) AS slice_count
+FROM thread_track
+JOIN _slice_track_summary USING (id)
+GROUP BY utid;
+
+CREATE PERFETTO TABLE _perf_sample_summary AS
+SELECT utid, count() AS perf_sample_cnt
+FROM perf_sample
+WHERE callsite_id IS NOT NULL
+GROUP BY utid;
+
+CREATE PERFETTO TABLE _thread_available_info_summary AS
+WITH raw AS (
+  SELECT
+    utid,
+    ss.max_running_dur,
+    ss.sum_running_dur,
+    ss.running_count,
+    (
+      SELECT slice_count
+      FROM _thread_track_summary
+      WHERE utid = t.utid
+    ) AS slice_count,
+    (
+      SELECT perf_sample_cnt
+      FROM _perf_sample_summary
+      WHERE utid = t.utid
+    ) AS perf_sample_count
+  FROM thread t
+  LEFT JOIN _sched_summary ss USING (utid)
+)
+SELECT
+  utid,
+  IFNULL(max_running_dur, 0) AS max_running_dur,
+  IFNULL(sum_running_dur, 0) AS sum_running_dur,
+  IFNULL(running_count, 0) AS running_count,
+  IFNULL(slice_count, 0) AS slice_count,
+  IFNULL(perf_sample_count, 0) AS perf_sample_count
+FROM raw r
+WHERE
+  NOT (
+    r.max_running_dur IS NULL
+    AND r.sum_running_dur IS NULL
+    AND r.running_count IS NULL
+    AND r.slice_count IS NULL
+    AND r.perf_sample_count IS NULL
+  )
+  OR utid IN (SELECT utid FROM cpu_profile_stack_sample)
+  OR utid IN (SELECT utid FROM thread_counter_track);
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
new file mode 100644
index 0000000..7a1d5d0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
@@ -0,0 +1,36 @@
+--
+-- 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 viz.summary.slices;
+
+CREATE PERFETTO TABLE _process_track_summary_by_upid_and_name AS
+SELECT
+  upid,
+  name,
+  GROUP_CONCAT(id) AS track_ids,
+  COUNT() AS track_count
+FROM process_track
+JOIN _slice_track_summary USING (id)
+GROUP BY upid, name;
+
+CREATE PERFETTO TABLE _uid_track_track_summary_by_uid_and_name AS
+SELECT
+  uid,
+  name,
+  GROUP_CONCAT(id) AS track_ids,
+  COUNT() AS track_count
+FROM uid_track
+JOIN _slice_track_summary USING (id)
+GROUP BY uid, name;
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index b937e2e..e8159c3 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -211,18 +211,21 @@
       return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
-    case TimestampedEvent::Type::kFtraceEvent:
     case TimestampedEvent::Type::kEtwEvent:
+    case TimestampedEvent::Type::kFtraceEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
 }
 
-void TraceSorter::ParseEtwPacket(TraceParser* /*parser*/,
-                                 uint32_t /*cpu*/,
+void TraceSorter::ParseEtwPacket(TraceParser* parser,
+                                 uint32_t cpu,
                                  const TimestampedEvent& event) {
+  TraceTokenBuffer::Id id = GetTokenBufferId(event);
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
     case TimestampedEvent::Type::kEtwEvent:
+      parser->ParseEtwEvent(cpu, event.ts,
+                            token_buffer_.Extract<TracePacketData>(id));
       return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index eb61280..dc64f34 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -138,20 +138,22 @@
   return stmt;
 }
 
-base::StatusOr<SqlValue> SqliteValueToSqlValueChecked(sqlite3_value* value,
-                                                      const Constraint& cs) {
-  SqlValue v = sqlite::utils::SqliteValueToSqlValue(value);
+int SqliteValueToSqlValueChecked(SqlValue* sql_val,
+                                 sqlite3_value* value,
+                                 const Constraint& cs,
+                                 sqlite3_vtab* vtab) {
+  *sql_val = sqlite::utils::SqliteValueToSqlValue(value);
   if constexpr (regex::IsRegexSupported()) {
     if (cs.op == FilterOp::kRegex) {
       if (cs.value.type != SqlValue::kString) {
-        return base::ErrStatus("Value has to be a string");
+        return sqlite::utils::SetError(vtab, "Value has to be a string");
       }
       if (auto st = regex::Regex::Create(cs.value.AsString()); !st.ok()) {
-        return st.status();
+        return sqlite::utils::SetError(vtab, st.status().c_message());
       }
     }
   }
-  return v;
+  return SQLITE_OK;
 }
 
 int UpdateConstraintsAndOrderByFromIndex(DbSqliteModule::Cursor* c,
@@ -166,18 +168,18 @@
 
   // We reuse this vector to reduce memory allocations on nested subqueries.
   uint32_t c_offset = 0;
-  c->constraints.resize(cs_count);
-  for (auto& cs : c->constraints) {
+  c->query.constraints.resize(cs_count);
+  for (auto& cs : c->query.constraints) {
     PERFETTO_CHECK(splitter.Next());
     cs.col_idx = *base::CStringToUInt32(splitter.cur_token());
     PERFETTO_CHECK(splitter.Next());
     cs.op = static_cast<FilterOp>(*base::CStringToUInt32(splitter.cur_token()));
 
-    auto value_or = SqliteValueToSqlValueChecked(argv[c_offset++], cs);
-    if (!value_or.ok()) {
-      return sqlite::utils::SetError(c->pVtab, value_or.status().c_message());
+    if (int ret = SqliteValueToSqlValueChecked(&cs.value, argv[c_offset++], cs,
+                                               c->pVtab);
+        ret != SQLITE_OK) {
+      return ret;
     }
-    cs.value = *value_or;
   }
 
   PERFETTO_CHECK(splitter.Next());
@@ -187,8 +189,8 @@
   uint32_t ob_count = *base::CStringToUInt32(splitter.cur_token() + 1);
 
   // We reuse this vector to reduce memory allocations on nested subqueries.
-  c->orders.resize(ob_count);
-  for (auto& ob : c->orders) {
+  c->query.orders.resize(ob_count);
+  for (auto& ob : c->query.orders) {
     PERFETTO_CHECK(splitter.Next());
     ob.col_idx = *base::CStringToUInt32(splitter.cur_token());
     PERFETTO_CHECK(splitter.Next());
@@ -216,13 +218,13 @@
 
   // If we have more than one constraint, we can't cache the table using
   // this method.
-  if (cursor->constraints.size() != 1) {
+  if (cursor->query.constraints.size() != 1) {
     return;
   }
 
   // If the constraing is not an equality constraint, there's little
   // benefit to caching
-  const auto& c = cursor->constraints.front();
+  const auto& c = cursor->query.constraints.front();
   if (c.op != FilterOp::kEq) {
     return;
   }
@@ -242,7 +244,7 @@
                             DbSqliteModule::Cursor* cursor,
                             metatrace::Record* r) {
   r->AddArg("Table", table_name);
-  for (const Constraint& c : cursor->constraints) {
+  for (const Constraint& c : cursor->query.constraints) {
     SafeStringWriter writer;
     writer.AppendString(schema.columns[c.col_idx].name);
 
@@ -303,7 +305,7 @@
     r->AddArg("Constraint", writer.GetStringView());
   }
 
-  for (const auto& o : cursor->orders) {
+  for (const auto& o : cursor->query.orders) {
     SafeStringWriter writer;
     writer.AppendString(schema.columns[o.col_idx].name);
     if (o.desc)
@@ -571,12 +573,12 @@
   size_t offset = c->table_function_arguments.size();
   bool is_same_idx = idx_num == c->last_idx_num;
   if (PERFETTO_LIKELY(is_same_idx)) {
-    for (auto& cs : c->constraints) {
-      auto value_or = SqliteValueToSqlValueChecked(argv[offset++], cs);
-      if (!value_or.ok()) {
-        return sqlite::utils::SetError(c->pVtab, value_or.status().c_message());
+    for (auto& cs : c->query.constraints) {
+      if (int ret = SqliteValueToSqlValueChecked(&cs.value, argv[offset++], cs,
+                                                 c->pVtab);
+          ret != SQLITE_OK) {
+        return ret;
       }
-      cs.value = *value_or;
     }
   } else {
     if (int r = UpdateConstraintsAndOrderByFromIndex(c, idx_str, argv + offset);
@@ -623,7 +625,7 @@
 
   const auto* source_table =
       c->sorted_cache_table ? &*c->sorted_cache_table : c->upstream_table;
-  RowMap filter_map = source_table->QueryToRowMap(c->constraints, c->orders);
+  RowMap filter_map = source_table->QueryToRowMap(c->query);
   if (filter_map.IsRange() && filter_map.size() <= 1) {
     // Currently, our criteria where we have a special fast path is if it's
     // a single ranged row. We have this fast path for joins on id columns
diff --git a/src/trace_processor/sqlite/db_sqlite_table.h b/src/trace_processor/sqlite/db_sqlite_table.h
index 401c370..a94cbe6 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.h
+++ b/src/trace_processor/sqlite/db_sqlite_table.h
@@ -109,8 +109,8 @@
     Mode mode = Mode::kSingleRow;
 
     int last_idx_num = -1;
-    std::vector<Constraint> constraints;
-    std::vector<Order> orders;
+    Query query;
+
     std::vector<SqlValue> table_function_arguments;
   };
   struct QueryCost {
diff --git a/src/trace_processor/sqlite/sqlite_engine.cc b/src/trace_processor/sqlite/sqlite_engine.cc
index cfd73dc..fcd68e8 100644
--- a/src/trace_processor/sqlite/sqlite_engine.cc
+++ b/src/trace_processor/sqlite/sqlite_engine.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/sqlite/sqlite_engine.h"
 
+#include <sqlite3.h>
 #include <cstdint>
 #include <optional>
 #include <string>
@@ -47,7 +48,15 @@
   // sqlite3_initialize isn't actually thread-safe in standalone builds because
   // we build with SQLITE_THREADSAFE=0. Ensure it's only called from a single
   // thread.
-  static bool init_once = [] { return sqlite3_initialize() == SQLITE_OK; }();
+  static bool init_once = [] {
+    // Enabling memstatus causes a lock to be taken on every malloc/free in
+    // SQLite to update the memory statistics. This can cause massive contention
+    // in trace processor when multiple instances are used in parallel.
+    // Fix this by disabling the memstatus API which we don't make use of in
+    // any case. See b/335019324 for more info on this.
+    PERFETTO_CHECK(sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0) == SQLITE_OK);
+    return sqlite3_initialize() == SQLITE_OK;
+  }();
   PERFETTO_CHECK(init_once);
 }
 
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 21b6090..792ad4f 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -885,8 +885,9 @@
                           const char* key,
                           std::optional<Variadic>* result) const {
     const auto& args = arg_table();
-    RowMap filtered = args.QueryToRowMap(
-        {args.arg_set_id().eq(arg_set_id), args.key().eq(key)}, {});
+    Query q;
+    q.constraints = {args.arg_set_id().eq(arg_set_id), args.key().eq(key)};
+    RowMap filtered = args.QueryToRowMap(q);
     if (filtered.empty()) {
       *result = std::nullopt;
       return util::OkStatus();
diff --git a/src/trace_processor/tables/BUILD.gn b/src/trace_processor/tables/BUILD.gn
index 8339d95..7cf8bb0 100644
--- a/src/trace_processor/tables/BUILD.gn
+++ b/src/trace_processor/tables/BUILD.gn
@@ -77,6 +77,7 @@
       "../../../include/perfetto/trace_processor",
       "../containers",
       "../db",
+      "../db/column",
     ]
     sources = [ "py_tables_benchmark.cc" ]
   }
diff --git a/src/trace_processor/tables/py_tables_benchmark.cc b/src/trace_processor/tables/py_tables_benchmark.cc
index 6aea3d5..ed19da1 100644
--- a/src/trace_processor/tables/py_tables_benchmark.cc
+++ b/src/trace_processor/tables/py_tables_benchmark.cc
@@ -22,6 +22,7 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/containers/row_map.h"
 #include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/db/column/types.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/tables/py_tables_benchmark_py.h"
 
@@ -56,6 +57,7 @@
 
 }  // namespace
 
+using perfetto::trace_processor::Query;
 using perfetto::trace_processor::RowMap;
 using perfetto::trace_processor::SqlValue;
 using perfetto::trace_processor::StringPool;
@@ -98,14 +100,15 @@
 static void BM_TableFilterRootId(benchmark::State& state) {
   StringPool pool;
   RootTestTable root(&pool);
+  Query q;
+  q.constraints = {root.id().eq(30)};
 
   uint32_t size = static_cast<uint32_t>(state.range(0));
   for (uint32_t i = 0; i < size; ++i)
     root.Insert({});
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(
-        root.ApplyAndIterateRows(root.QueryToRowMap({root.id().eq(30)}, {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterRootId)->Apply(TableFilterArgs);
@@ -113,6 +116,9 @@
 static void BM_TableFilterRootIdAndOther(benchmark::State& state) {
   StringPool pool;
   RootTestTable root(&pool);
+  Query q;
+  q.constraints = {root.id().eq(root.row_count() - 1),
+                   root.root_non_null().gt(100)};
 
   uint32_t size = static_cast<uint32_t>(state.range(0));
 
@@ -123,9 +129,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(
-        {root.id().eq(root.row_count() - 1), root.root_non_null().gt(100)},
-        {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterRootIdAndOther)->Apply(TableFilterArgs);
@@ -134,6 +138,8 @@
   StringPool pool;
   RootTestTable root(&pool);
   ChildTestTable child(&pool, &root);
+  Query q;
+  q.constraints = {child.id().eq(30)};
 
   uint32_t size = static_cast<uint32_t>(state.range(0));
   for (uint32_t i = 0; i < size; ++i) {
@@ -142,8 +148,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.id().eq(30)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildId)->Apply(TableFilterArgs);
@@ -152,6 +157,8 @@
   StringPool pool;
   RootTestTable root(&pool);
   ChildTestTable child(&pool, &root);
+  Query q;
+  q.constraints = {child.id().eq(30), child.root_sorted().gt(1024)};
 
   uint32_t size = static_cast<uint32_t>(state.range(0));
   for (uint32_t i = 0; i < size; ++i) {
@@ -165,8 +172,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(
-        {child.id().eq(30), child.root_sorted().gt(1024)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildIdAndSortedInRoot)->Apply(TableFilterArgs);
@@ -174,6 +180,8 @@
 static void BM_TableFilterRootNonNullEqMatchMany(benchmark::State& state) {
   StringPool pool;
   RootTestTable root(&pool);
+  Query q;
+  q.constraints = {root.root_non_null().eq(0)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 1024;
@@ -185,8 +193,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(
-        root.QueryToRowMap({root.root_non_null().eq(0)}, {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterRootNonNullEqMatchMany)->Apply(TableFilterArgs);
@@ -194,6 +201,8 @@
 static void BM_TableFilterRootMultipleNonNull(benchmark::State& state) {
   StringPool pool;
   RootTestTable root(&pool);
+  Query q;
+  q.constraints = {root.root_non_null().lt(4), root.root_non_null_2().lt(10)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 512;
@@ -207,8 +216,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(
-        {root.root_non_null().lt(4), root.root_non_null_2().lt(10)}, {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterRootMultipleNonNull)->Apply(TableFilterArgs);
@@ -216,6 +224,8 @@
 static void BM_TableFilterRootNullableEqMatchMany(benchmark::State& state) {
   StringPool pool;
   RootTestTable root(&pool);
+  Query q;
+  q.constraints = {root.root_nullable().eq(1)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 512;
@@ -231,8 +241,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(
-        root.QueryToRowMap({root.root_nullable().eq(1)}, {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterRootNullableEqMatchMany)->Apply(TableFilterArgs);
@@ -241,6 +250,8 @@
   StringPool pool;
   RootTestTable root(&pool);
   ChildTestTable child(&pool, &root);
+  Query q;
+  q.constraints = {child.child_non_null().eq(0)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 1024;
@@ -254,8 +265,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.child_non_null().eq(0)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildNonNullEqMatchMany)->Apply(TableFilterArgs);
@@ -264,6 +274,8 @@
   StringPool pool;
   RootTestTable root(&pool);
   ChildTestTable child(&pool, &root);
+  Query q;
+  q.constraints = {child.child_nullable().eq(1)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 512;
@@ -280,8 +292,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.child_nullable().eq(1)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildNullableEqMatchMany)->Apply(TableFilterArgs);
@@ -291,6 +302,8 @@
   StringPool pool;
   RootTestTable root(&pool);
   ChildTestTable child(&pool, &root);
+  Query q;
+  q.constraints = {child.root_non_null().eq(0)};
 
   auto size = static_cast<uint32_t>(state.range(0));
   uint32_t partitions = size / 1024;
@@ -304,8 +317,7 @@
   }
 
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.root_non_null().eq(0)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildNonNullEqMatchManyInParent)
@@ -328,9 +340,10 @@
     child.Insert(row);
   }
 
+  Query q;
+  q.constraints = {child.root_nullable().eq(1)};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.root_nullable().eq(1)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildNullableEqMatchManyInParent)
@@ -348,9 +361,10 @@
     root.Insert(row);
   }
 
+  Query q;
+  q.constraints = {root.root_sorted().eq(22)};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(
-        root.QueryToRowMap({root.root_sorted().eq(22)}, {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterParentSortedEq)->Apply(TableFilterArgs);
@@ -373,10 +387,11 @@
   // We choose to search for the last group as if there is O(n^2), it will
   // be more easily visible.
   uint32_t last_group = ((size - 1) / 10) * 10;
+  Query q;
+  q.constraints = {root.root_sorted().eq(last_group),
+                   root.root_non_null().eq(size - 1)};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(
-        {root.root_sorted().eq(last_group), root.root_non_null().eq(size - 1)},
-        {})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterParentSortedAndOther)->Apply(TableFilterArgs);
@@ -395,9 +410,10 @@
     child.Insert(row);
   }
 
+  Query q;
+  q.constraints = {child.child_sorted().eq(22)};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.child_sorted().eq(22)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildSortedEq)->Apply(TableFilterArgs);
@@ -419,9 +435,10 @@
     child.Insert(row);
   }
 
+  Query q;
+  q.constraints = {child.root_sorted().eq(22)};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({child.root_sorted().eq(22)}, {})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableFilterChildSortedEqInParent)->Apply(TableFilterArgs);
@@ -441,9 +458,10 @@
     root.Insert(row);
   }
 
+  Query q;
+  q.orders = {root.root_non_null().ascending()};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(
-        root.QueryToRowMap({}, {root.root_non_null().ascending()})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableSortRootNonNull)->Apply(TableSortArgs);
@@ -464,9 +482,10 @@
     root.Insert(row);
   }
 
+  Query q;
+  q.orders = {root.root_nullable().ascending()};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(root.ApplyAndIterateRows(
-        root.QueryToRowMap({}, {root.root_nullable().ascending()})));
+    benchmark::DoNotOptimize(root.ApplyAndIterateRows(root.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableSortRootNullable)->Apply(TableSortArgs);
@@ -493,9 +512,10 @@
     child.Insert(child_row);
   }
 
+  Query q;
+  q.orders = {child.root_non_null().ascending()};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({}, {child.root_non_null().ascending()})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableSortChildNonNullInParent)->Apply(TableSortArgs);
@@ -524,9 +544,10 @@
     child.Insert(child_row);
   }
 
+  Query q;
+  q.orders = {child.root_nullable().ascending()};
   for (auto _ : state) {
-    benchmark::DoNotOptimize(child.ApplyAndIterateRows(
-        child.QueryToRowMap({}, {child.root_nullable().ascending()})));
+    benchmark::DoNotOptimize(child.ApplyAndIterateRows(child.QueryToRowMap(q)));
   }
 }
 BENCHMARK(BM_TableSortChildNullableInParent)->Apply(TableSortArgs);
diff --git a/src/trace_processor/tables/py_tables_unittest.cc b/src/trace_processor/tables/py_tables_unittest.cc
index 09056d8..1f4a4ef 100644
--- a/src/trace_processor/tables/py_tables_unittest.cc
+++ b/src/trace_processor/tables/py_tables_unittest.cc
@@ -243,14 +243,16 @@
   // Verify that not-present ids are not returned.
   {
     static constexpr uint32_t kFilterArgSetId = 1;
-    auto res =
-        table.QueryToRowMap({table.arg_set_id().eq(kFilterArgSetId)}, {});
+    Query q;
+    q.constraints = {table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_TRUE(res.empty());
   }
   {
     static constexpr uint32_t kFilterArgSetId = 9;
-    auto res =
-        table.QueryToRowMap({table.arg_set_id().eq(kFilterArgSetId)}, {});
+    Query q;
+    q.constraints = {table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_TRUE(res.empty());
   }
 
@@ -260,8 +262,9 @@
   // Verify that filtering equality for real arg set ids works as expected.
   {
     static constexpr uint32_t kFilterArgSetId = 4;
-    auto res =
-        table.QueryToRowMap({table.arg_set_id().eq(kFilterArgSetId)}, {});
+    Query q;
+    q.constraints = {table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_EQ(res.size(), 4u);
     for (auto it = table.ApplyAndIterateRows(std::move(res)); it; ++it) {
       auto arg_set_id =
@@ -271,8 +274,9 @@
   }
   {
     static constexpr uint32_t kFilterArgSetId = 0;
-    auto res =
-        table.QueryToRowMap({table.arg_set_id().eq(kFilterArgSetId)}, {});
+    Query q;
+    q.constraints = {table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_EQ(res.size(), 2u);
     for (auto it = table.ApplyAndIterateRows(std::move(res)); it; ++it) {
       auto arg_set_id =
@@ -282,8 +286,9 @@
   }
   {
     static constexpr uint32_t kFilterArgSetId = 8;
-    auto res =
-        table.QueryToRowMap({table.arg_set_id().eq(kFilterArgSetId)}, {});
+    Query q;
+    q.constraints = {table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_EQ(res.size(), 1u);
     for (auto it = table.ApplyAndIterateRows(std::move(res)); it; ++it) {
       auto arg_set_id =
@@ -296,9 +301,10 @@
   // column works.
   {
     static constexpr uint32_t kFilterArgSetId = 4;
-    auto res = table.QueryToRowMap(
-        {table.int_value().eq(200), table.arg_set_id().eq(kFilterArgSetId)},
-        {});
+    Query q;
+    q.constraints = {table.int_value().eq(200),
+                     table.arg_set_id().eq(kFilterArgSetId)};
+    auto res = table.QueryToRowMap(q);
     ASSERT_EQ(res.size(), 2u);
     for (auto it = table.ApplyAndIterateRows(std::move(res)); it; ++it) {
       uint32_t arg_set_id =
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index b36a63a..30c83e6 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -81,6 +81,8 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/utils.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/slice_mipmap_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.h"
@@ -747,6 +749,12 @@
       std::make_unique<SpanJoinOperatorModule::Context>(engine_.get()));
   engine_->sqlite_engine()->RegisterVirtualTableModule<WindowOperatorModule>(
       "window", std::make_unique<WindowOperatorModule::Context>());
+  engine_->sqlite_engine()->RegisterVirtualTableModule<CounterMipmapOperator>(
+      "__intrinsic_counter_mipmap",
+      std::make_unique<CounterMipmapOperator::Context>(engine_.get()));
+  engine_->sqlite_engine()->RegisterVirtualTableModule<SliceMipmapOperator>(
+      "__intrinsic_slice_mipmap",
+      std::make_unique<SliceMipmapOperator::Context>(engine_.get()));
 
   // Initalize the tables and views in the prelude.
   InitializePreludeTablesViews(db);
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index af2837f..3136835 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -131,22 +131,23 @@
   // the GetOrCreate() method on their subclass type, e.g.
   // SyscallTracker::GetOrCreate(context)
   // clang-format off
-  std::unique_ptr<Destructible> android_probes_tracker;  // AndroidProbesTracker
-  std::unique_ptr<Destructible> binder_tracker;          // BinderTracker
-  std::unique_ptr<Destructible> heap_graph_tracker;      // HeapGraphTracker
-  std::unique_ptr<Destructible> syscall_tracker;         // SyscallTracker
-  std::unique_ptr<Destructible> system_info_tracker;     // SystemInfoTracker
-  std::unique_ptr<Destructible> v4l2_tracker;            // V4l2Tracker
-  std::unique_ptr<Destructible> virtio_video_tracker;    // VirtioVideoTracker
-  std::unique_ptr<Destructible> systrace_parser;         // SystraceParser
-  std::unique_ptr<Destructible> thread_state_tracker;    // ThreadStateTracker
-  std::unique_ptr<Destructible> i2c_tracker;             // I2CTracker
-  std::unique_ptr<Destructible> perf_data_tracker;       // PerfDataTracker
-  std::unique_ptr<Destructible> content_analyzer;        // ProtoContentAnalyzer
+  std::unique_ptr<Destructible> android_probes_tracker;    // AndroidProbesTracker
+  std::unique_ptr<Destructible> binder_tracker;            // BinderTracker
+  std::unique_ptr<Destructible> heap_graph_tracker;        // HeapGraphTracker
+  std::unique_ptr<Destructible> syscall_tracker;           // SyscallTracker
+  std::unique_ptr<Destructible> system_info_tracker;       // SystemInfoTracker
+  std::unique_ptr<Destructible> v4l2_tracker;              // V4l2Tracker
+  std::unique_ptr<Destructible> virtio_video_tracker;      // VirtioVideoTracker
+  std::unique_ptr<Destructible> systrace_parser;           // SystraceParser
+  std::unique_ptr<Destructible> thread_state_tracker;      // ThreadStateTracker
+  std::unique_ptr<Destructible> i2c_tracker;               // I2CTracker
+  std::unique_ptr<Destructible> perf_data_tracker;         // PerfDataTracker
+  std::unique_ptr<Destructible> content_analyzer;          // ProtoContentAnalyzer
   std::unique_ptr<Destructible> shell_transitions_tracker; // ShellTransitionsTracker
-  std::unique_ptr<Destructible> ftrace_sched_tracker;    // FtraceSchedEventTracker
-  std::unique_ptr<Destructible> v8_tracker;              // V8Tracker
-  std::unique_ptr<Destructible> jit_tracker;             // JitTracker
+  std::unique_ptr<Destructible> protolog_messages_tracker; // ProtoLogMessagesTracker
+  std::unique_ptr<Destructible> ftrace_sched_tracker;      // FtraceSchedEventTracker
+  std::unique_ptr<Destructible> v8_tracker;                // V8Tracker
+  std::unique_ptr<Destructible> jit_tracker;               // JitTracker
   // clang-format on
 
   // These fields are trace readers which will be called by |forwarding_parser|
diff --git a/src/trace_processor/util/profile_builder.cc b/src/trace_processor/util/profile_builder.cc
index 951472b..6948a8e 100644
--- a/src/trace_processor/util/profile_builder.cc
+++ b/src/trace_processor/util/profile_builder.cc
@@ -466,9 +466,9 @@
   using RowRef =
       perfetto::trace_processor::tables::SymbolTable::ConstRowReference;
   std::vector<RowRef> symbol_set;
-  for (auto it = symbols.FilterToIterator(
-           {symbols.symbol_set_id().eq(*symbol_set_id)});
-       it; ++it) {
+  Query q;
+  q.constraints = {symbols.symbol_set_id().eq(*symbol_set_id)};
+  for (auto it = symbols.FilterToIterator(q); it; ++it) {
     symbol_set.push_back(it.row_reference());
   }
 
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
index e876fac..4f2fb5f 100644
--- a/src/trace_redaction/BUILD.gn
+++ b/src/trace_redaction/BUILD.gn
@@ -28,8 +28,10 @@
 
 source_set("trace_redaction") {
   sources = [
-    "build_timeline.cc",
-    "build_timeline.h",
+    "collect_frame_cookies.cc",
+    "collect_frame_cookies.h",
+    "collect_timeline_events.cc",
+    "collect_timeline_events.h",
     "filter_ftrace_using_allowlist.cc",
     "filter_ftrace_using_allowlist.h",
     "filter_packet_using_allowlist.cc",
@@ -42,6 +44,7 @@
     "filter_task_rename.h",
     "find_package_uid.cc",
     "find_package_uid.h",
+    "frame_cookie.h",
     "optimize_timeline.cc",
     "optimize_timeline.h",
     "populate_allow_lists.cc",
@@ -68,6 +71,8 @@
     "scrub_process_trees.h",
     "scrub_trace_packet.cc",
     "scrub_trace_packet.h",
+    "suspend_resume.cc",
+    "suspend_resume.h",
     "trace_redaction_framework.cc",
     "trace_redaction_framework.h",
     "trace_redactor.cc",
@@ -84,6 +89,7 @@
     "../../protos/perfetto/trace/ftrace:zero",
     "../../protos/perfetto/trace/ps:zero",
     "../trace_processor:storage_minimal",
+    "../trace_processor/util:util",
   ]
 }
 
@@ -93,20 +99,22 @@
     "filter_ftrace_using_allowlist_integrationtest.cc",
     "filter_sched_waking_events_integrationtest.cc",
     "filter_task_rename_integrationtest.cc",
+    "prune_package_list_integrationtest.cc",
     "redact_sched_switch_integrationtest.cc",
     "scrub_ftrace_events_integrationtest.cc",
     "scrub_process_stats_integrationtest.cc",
     "scrub_process_trees_integrationtest.cc",
     "trace_redaction_integration_fixture.cc",
     "trace_redaction_integration_fixture.h",
-    "trace_redactor_integrationtest.cc",
   ]
   deps = [
     ":trace_redaction",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
     "../../include/perfetto/ext/base",
+    "../../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",
@@ -117,7 +125,8 @@
 perfetto_unittest_source_set("unittests") {
   testonly = true
   sources = [
-    "build_timeline_unittest.cc",
+    "collect_frame_cookies_unittest.cc",
+    "collect_timeline_events_unittest.cc",
     "filter_ftrace_using_allowlist_unittest.cc",
     "filter_packet_using_allowlist_unittest.cc",
     "filter_sched_waking_events_unittest.cc",
@@ -129,17 +138,20 @@
     "redact_process_free_unittest.cc",
     "redact_sched_switch_unittest.cc",
     "redact_task_newtask_unittest.cc",
+    "suspend_resume_unittest.cc",
   ]
   deps = [
     ":trace_redaction",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
+    "../../include/perfetto/ext/base:base",
     "../../include/perfetto/protozero:protozero",
     "../../protos/perfetto/config:cpp",
     "../../protos/perfetto/config:zero",
     "../../protos/perfetto/trace:non_minimal_cpp",
     "../../protos/perfetto/trace:zero",
     "../../protos/perfetto/trace/android:cpp",
+    "../../protos/perfetto/trace/android:zero",
     "../../protos/perfetto/trace/ftrace:cpp",
     "../../protos/perfetto/trace/ftrace:zero",
     "../../protos/perfetto/trace/ps:cpp",
diff --git a/src/trace_redaction/collect_frame_cookies.cc b/src/trace_redaction/collect_frame_cookies.cc
new file mode 100644
index 0000000..13e5539
--- /dev/null
+++ b/src/trace_redaction/collect_frame_cookies.cc
@@ -0,0 +1,212 @@
+/*
+ * 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/collect_frame_cookies.h"
+
+#include "perfetto/base/status.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/android/frame_timeline_event.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+using FrameTimelineEvent = protos::pbzero::FrameTimelineEvent;
+
+struct Frame {
+  uint32_t id;
+  uint32_t pid;
+  uint32_t cookie;
+};
+
+constexpr Frame kActualDisplayFrameStart = {
+    FrameTimelineEvent::kActualDisplayFrameStartFieldNumber,
+    FrameTimelineEvent::ActualDisplayFrameStart::kPidFieldNumber,
+    FrameTimelineEvent::ActualDisplayFrameStart::kCookieFieldNumber,
+};
+
+constexpr Frame kExpectedDisplayFrameStart = {
+    FrameTimelineEvent::kExpectedDisplayFrameStartFieldNumber,
+    FrameTimelineEvent::ExpectedDisplayFrameStart::kPidFieldNumber,
+    FrameTimelineEvent::ExpectedDisplayFrameStart::kCookieFieldNumber,
+};
+
+constexpr Frame kActualSurfaceFrameStart = {
+    FrameTimelineEvent::kActualSurfaceFrameStartFieldNumber,
+    FrameTimelineEvent::ActualSurfaceFrameStart::kPidFieldNumber,
+    FrameTimelineEvent::ActualSurfaceFrameStart::kCookieFieldNumber,
+};
+
+constexpr Frame kExpectedSurfaceFrameStart = {
+    FrameTimelineEvent::kExpectedSurfaceFrameStartFieldNumber,
+    FrameTimelineEvent::ExpectedSurfaceFrameStart::kPidFieldNumber,
+    FrameTimelineEvent::ExpectedSurfaceFrameStart::kCookieFieldNumber,
+};
+
+// Do not use `pid` from `kFrameEnd`.
+constexpr Frame kFrameEnd = {
+    FrameTimelineEvent::kFrameEndFieldNumber,
+    0,
+    FrameTimelineEvent::FrameEnd::kCookieFieldNumber,
+};
+
+}  // namespace
+
+base::Status CollectFrameCookies::Begin(Context* context) const {
+  if (context->global_frame_cookies.empty()) {
+    return base::OkStatus();
+  }
+
+  return base::ErrStatus("FindFrameCookies: frame cookies already populated");
+}
+
+base::Status CollectFrameCookies::Collect(
+    const protos::pbzero::TracePacket::Decoder& packet,
+    Context* context) const {
+  // A frame cookie needs a time and pid for a timeline query. Ignore packets
+  // without a timestamp.
+  if (!packet.has_timestamp() || !packet.has_frame_timeline_event()) {
+    return base::OkStatus();
+  }
+
+  auto timestamp = packet.timestamp();
+
+  // Only use the start frames. They are the only ones with a pid. End events
+  // use the cookies to reference the pid in a start event.
+  auto handlers = {
+      kActualDisplayFrameStart,
+      kActualSurfaceFrameStart,
+      kExpectedDisplayFrameStart,
+      kExpectedSurfaceFrameStart,
+  };
+
+  // Timeline Event Decoder.
+  protozero::ProtoDecoder decoder(packet.frame_timeline_event());
+
+  // If no handler worked, cookie will not get added to the global cookie field.
+  for (const auto& handler : handlers) {
+    auto outer = decoder.FindField(handler.id);
+
+    if (!outer.valid()) {
+      continue;
+    }
+
+    protozero::ProtoDecoder inner(outer.as_bytes());
+
+    auto pid = inner.FindField(handler.pid);
+    auto cookie = inner.FindField(handler.cookie);
+
+    // This should be handled, but it is not valid. Drop the event by not adding
+    // it to the global_frame_cookies list.
+    if (!pid.valid() || !cookie.valid()) {
+      continue;
+    }
+
+    FrameCookie frame_cookie;
+    frame_cookie.pid = pid.as_int32();
+    frame_cookie.cookie = cookie.as_int64();
+    frame_cookie.ts = timestamp;
+
+    context->global_frame_cookies.push_back(frame_cookie);
+
+    break;
+  }
+
+  return base::OkStatus();
+}
+
+base::Status ReduceFrameCookies::Build(Context* context) const {
+  if (!context->package_uid.has_value()) {
+    return base::ErrStatus("ReduceFrameCookies: missing package uid.");
+  }
+
+  if (!context->timeline) {
+    return base::ErrStatus("ReduceFrameCookies: missing timeline.");
+  }
+
+  // Even though it is rare, it is possible for there to be no SurfaceFlinger
+  // frame cookies. Even through the main path handles this, we use this early
+  // exit to document this edge case.
+  if (context->global_frame_cookies.empty()) {
+    return base::OkStatus();
+  }
+
+  const auto* timeline = context->timeline.get();
+  auto uid = context->package_uid.value();
+
+  auto& package_frame_cookies = context->package_frame_cookies;
+
+  // Filter the global cookies down to cookies that belong to the target package
+  // (uid).
+  for (const auto& cookie : context->global_frame_cookies) {
+    auto cookie_slice = timeline->Search(cookie.ts, cookie.pid);
+
+    if (cookie_slice.uid == uid) {
+      package_frame_cookies.insert(cookie.cookie);
+    }
+  }
+
+  return base::OkStatus();
+}
+
+bool FilterFrameEvents::KeepField(const Context& context,
+                                  const protozero::Field& field) const {
+  // If this field is not a timeline event, then this primitive has no reason to
+  // reject this field.
+  //
+  // If it is a timeline event, the event's cookie must be in the package's
+  // cookies.
+  if (field.id() !=
+      protos::pbzero::TracePacket::kFrameTimelineEventFieldNumber) {
+    return true;
+  }
+
+  protozero::ProtoDecoder timeline_event_decoder(field.as_bytes());
+
+  auto handlers = {
+      kActualDisplayFrameStart,
+      kActualSurfaceFrameStart,
+      kExpectedDisplayFrameStart,
+      kExpectedSurfaceFrameStart,
+      kFrameEnd,
+  };
+
+  const auto& cookies = context.package_frame_cookies;
+
+  for (const auto& handler : handlers) {
+    auto event = timeline_event_decoder.FindField(handler.id);
+
+    if (!event.valid()) {
+      continue;
+    }
+
+    protozero::ProtoDecoder event_decoder(event.as_bytes());
+
+    auto cookie = event_decoder.FindField(handler.cookie);
+
+    if (cookie.valid() && cookies.count(cookie.as_int64())) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/collect_frame_cookies.h b/src/trace_redaction/collect_frame_cookies.h
new file mode 100644
index 0000000..7463ff6
--- /dev/null
+++ b/src/trace_redaction/collect_frame_cookies.h
@@ -0,0 +1,60 @@
+/*
+ * 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_COLLECT_FRAME_COOKIES_H_
+#define SRC_TRACE_REDACTION_COLLECT_FRAME_COOKIES_H_
+
+#include "perfetto/protozero/field.h"
+#include "src/trace_redaction/scrub_trace_packet.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+// Populates Context::global_frame_cookies using FrameTimelineEvent messages.
+class CollectFrameCookies : public CollectPrimitive {
+ public:
+  base::Status Begin(Context* context) const override;
+
+  base::Status Collect(const protos::pbzero::TracePacket::Decoder& packet,
+                       Context* context) const override;
+
+ private:
+  void OnTimelineEvent(const protos::pbzero::TracePacket::Decoder& packet,
+                       protozero::ConstBytes bytes,
+                       Context* context) const;
+};
+
+// Moves cookies from Context::global_frame_cookies to
+// Context::package_frame_cookies using Cookies::timeline and
+// Cookies::package_uid.
+class ReduceFrameCookies : public BuildPrimitive {
+ public:
+  base::Status Build(Context* context) const override;
+};
+
+// Flags start-frame and end-frame events as keep/drop using
+// Context::package_frame_cookies.
+class FilterFrameEvents : public TracePacketFilter {
+ public:
+  bool KeepField(const Context& context,
+                 const protozero::Field& field) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_COLLECT_FRAME_COOKIES_H_
diff --git a/src/trace_redaction/collect_frame_cookies_unittest.cc b/src/trace_redaction/collect_frame_cookies_unittest.cc
new file mode 100644
index 0000000..6e77231
--- /dev/null
+++ b/src/trace_redaction/collect_frame_cookies_unittest.cc
@@ -0,0 +1,346 @@
+
+/*
+ * 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/collect_frame_cookies.h"
+#include "src/base/test/status_matchers.h"
+#include "src/trace_redaction/collect_timeline_events.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/android/frame_timeline_event.gen.h"
+#include "protos/perfetto/trace/android/frame_timeline_event.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+constexpr uint64_t kTimestampA = 0;
+constexpr uint64_t kTimestampB = 1000;
+constexpr uint64_t kTimestampC = 2000;
+constexpr uint64_t kTimestampD = 3000;
+constexpr uint64_t kTimestampE = 3000;
+
+constexpr int64_t kCookieA = 1234;
+
+// Start at 1, amd not zero, because zero hnas special meaning (system uid).
+constexpr uint64_t kUidA = 1;
+
+constexpr int32_t kPidNone = 10;
+constexpr int32_t kPidA = 11;
+
+}  // namespace
+
+class FrameCookieFixture {
+ protected:
+  std::string CreateStartEvent(int32_t field_id,
+                               uint64_t ts,
+                               int32_t pid,
+                               int64_t cookie) const {
+    protos::gen::TracePacket packet;
+    packet.set_timestamp(ts);
+
+    switch (field_id) {
+      case protos::pbzero::FrameTimelineEvent::
+          kExpectedSurfaceFrameStartFieldNumber:
+        CreateExpectedSurfaceFrameStart(pid, cookie,
+                                        packet.mutable_frame_timeline_event());
+        break;
+
+      case protos::pbzero::FrameTimelineEvent::
+          kActualSurfaceFrameStartFieldNumber:
+        CreateActualSurfaceFrameStart(pid, cookie,
+                                      packet.mutable_frame_timeline_event());
+        break;
+
+      case protos::pbzero::FrameTimelineEvent::
+          kExpectedDisplayFrameStartFieldNumber:
+        CreateExpectedDisplayFrameStart(pid, cookie,
+                                        packet.mutable_frame_timeline_event());
+        break;
+
+      case protos::pbzero::FrameTimelineEvent::
+          kActualDisplayFrameStartFieldNumber:
+        CreateActualDisplayFrameStart(pid, cookie,
+                                      packet.mutable_frame_timeline_event());
+        break;
+
+      default:
+        PERFETTO_FATAL("Invalid field id");
+        break;
+    }
+
+    return packet.SerializeAsString();
+  }
+
+  std::string CreateFrameEnd(uint64_t ts, int64_t cookie) const {
+    protos::gen::TracePacket packet;
+    packet.set_timestamp(ts);
+
+    auto* start = packet.mutable_frame_timeline_event()->mutable_frame_end();
+    start->set_cookie(cookie);
+
+    return packet.SerializeAsString();
+  }
+
+  void CollectEvents(std::initializer_list<ProcessThreadTimeline::Event> events,
+                     Context* context) const {
+    CollectTimelineEvents collect;
+    ASSERT_OK(collect.Begin(context));
+
+    for (const auto& event : events) {
+      context->timeline->Append(event);
+    }
+
+    ASSERT_OK(collect.End(context));
+  }
+
+  void CollectCookies(std::initializer_list<std::string> packets,
+                      Context* context) const {
+    CollectFrameCookies collect;
+    ASSERT_OK(collect.Begin(context));
+
+    for (const auto& packet : packets) {
+      protos::pbzero::TracePacket::Decoder decoder(packet);
+      ASSERT_OK(collect.Collect(decoder, context));
+    }
+
+    ASSERT_OK(collect.End(context));
+  }
+
+ private:
+  void CreateExpectedSurfaceFrameStart(
+      int32_t pid,
+      int64_t cookie,
+      protos::gen::FrameTimelineEvent* event) const {
+    auto* start = event->mutable_expected_surface_frame_start();
+    start->set_cookie(cookie);
+    start->set_pid(pid);
+  }
+
+  void CreateActualSurfaceFrameStart(
+      int32_t pid,
+      int64_t cookie,
+      protos::gen::FrameTimelineEvent* event) const {
+    auto* start = event->mutable_actual_surface_frame_start();
+    start->set_cookie(cookie);
+    start->set_pid(pid);
+  }
+
+  void CreateExpectedDisplayFrameStart(
+      int32_t pid,
+      int64_t cookie,
+      protos::gen::FrameTimelineEvent* event) const {
+    auto* start = event->mutable_expected_display_frame_start();
+    start->set_cookie(cookie);
+    start->set_pid(pid);
+  }
+
+  void CreateActualDisplayFrameStart(
+      int32_t pid,
+      int64_t cookie,
+      protos::gen::FrameTimelineEvent* event) const {
+    auto* start = event->mutable_actual_display_frame_start();
+    start->set_cookie(cookie);
+    start->set_pid(pid);
+  }
+};
+
+class CollectFrameCookiesTest : public testing::Test,
+                                protected FrameCookieFixture,
+                                public testing::WithParamInterface<int32_t> {
+ protected:
+  Context context_;
+};
+
+TEST_P(CollectFrameCookiesTest, ExtractsExpectedSurfaceFrameStart) {
+  auto field_id = GetParam();
+
+  auto packet = CreateStartEvent(field_id, kTimestampA, kPidA, kCookieA);
+
+  CollectCookies({packet}, &context_);
+
+  ASSERT_EQ(context_.global_frame_cookies.size(), 1u);
+
+  auto& cookie = context_.global_frame_cookies.back();
+  ASSERT_EQ(cookie.cookie, kCookieA);
+  ASSERT_EQ(cookie.pid, kPidA);
+  ASSERT_EQ(cookie.ts, kTimestampA);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EveryStartEventType,
+    CollectFrameCookiesTest,
+    testing::Values(
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::kActualSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedDisplayFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kActualDisplayFrameStartFieldNumber));
+
+// End events have no influence during the collect phase because they don't have
+// a direct connection to a process. They're indirectly connected to a pid via a
+// start event (via a common cookie value).
+TEST_F(CollectFrameCookiesTest, IgnoresFrameEnd) {
+  CollectCookies({CreateFrameEnd(kTimestampA, kPidA)}, &context_);
+
+  ASSERT_TRUE(context_.global_frame_cookies.empty());
+}
+
+class ReduceFrameCookiesTest : public testing::Test,
+                               protected FrameCookieFixture,
+                               public testing::WithParamInterface<int32_t> {
+ protected:
+  void SetUp() {
+    context_.package_uid = kUidA;
+
+    // Time A   +- Time B       +- Time C    +- Time D   +- Time E
+    //          |                            |
+    //          +------------ Pid A ---------+
+    //
+    // The pid will be active from time b to time d. Time A will be used for
+    // "before active". Time C will be used for "while active". Time E will be
+    // used for "after active".
+    CollectEvents(
+        {
+            ProcessThreadTimeline::Event::Open(kTimestampB, kPidA, kPidNone,
+                                               kUidA),
+            ProcessThreadTimeline::Event::Close(kTimestampD, kPidA),
+        },
+        &context_);
+  }
+
+  ReduceFrameCookies reduce_;
+  Context context_;
+};
+
+TEST_P(ReduceFrameCookiesTest, RejectBeforeActive) {
+  auto field_id = GetParam();
+
+  // kTimestampA is before pid starts.
+  auto packet = CreateStartEvent(field_id, kTimestampA, kPidA, kCookieA);
+
+  CollectCookies({packet}, &context_);
+
+  ASSERT_OK(reduce_.Build(&context_));
+  ASSERT_FALSE(context_.package_frame_cookies.count(kCookieA));
+}
+
+TEST_P(ReduceFrameCookiesTest, AcceptDuringActive) {
+  auto field_id = GetParam();
+
+  // kTimestampC is between pid starts and ends.
+  auto packet = CreateStartEvent(field_id, kTimestampC, kPidA, kCookieA);
+
+  CollectCookies({packet}, &context_);
+
+  ASSERT_OK(reduce_.Build(&context_));
+  ASSERT_TRUE(context_.package_frame_cookies.count(kCookieA));
+}
+
+TEST_P(ReduceFrameCookiesTest, RejectAfterActive) {
+  auto field_id = GetParam();
+
+  // kTimestampE is after pid ends.
+  auto packet = CreateStartEvent(field_id, kTimestampE, kPidA, kCookieA);
+
+  CollectCookies({packet}, &context_);
+
+  ASSERT_OK(reduce_.Build(&context_));
+  ASSERT_FALSE(context_.package_frame_cookies.count(kCookieA));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EveryStartEventType,
+    ReduceFrameCookiesTest,
+    testing::Values(
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::kActualSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedDisplayFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kActualDisplayFrameStartFieldNumber));
+
+class FilterCookiesFieldsTest : public testing::Test,
+                                protected FrameCookieFixture,
+                                public testing::WithParamInterface<int32_t> {
+ protected:
+  protozero::Field ExtractTimelineEvent(const std::string& packet) const {
+    protozero::ProtoDecoder packet_decoder(packet);
+
+    // There must be one in order for the test to work, so we assume it's there.
+    return packet_decoder.FindField(
+        protos::pbzero::TracePacket::kFrameTimelineEventFieldNumber);
+  }
+
+  FilterFrameEvents filter_;
+  Context context_;
+};
+
+// If the event was within a valid pid's lifespan and was connected to the
+// package, it should be kept.
+TEST_P(FilterCookiesFieldsTest, IncludeIncludedStartCookies) {
+  context_.package_frame_cookies.insert(kCookieA);
+
+  auto field_id = GetParam();
+  auto packet = CreateStartEvent(field_id, kTimestampA, kPidA, kCookieA);
+  auto timeline_field = ExtractTimelineEvent(packet);
+
+  ASSERT_TRUE(filter_.KeepField(context_, timeline_field));
+}
+
+// If the event wasn't within a valid pid's lifespans and/or was connected to a
+// package, it should be removed.
+TEST_P(FilterCookiesFieldsTest, ExcludeMissingStartCookies) {
+  auto field_id = GetParam();
+  auto packet = CreateStartEvent(field_id, kTimestampA, kPidA, kCookieA);
+  auto timeline_field = ExtractTimelineEvent(packet);
+
+  ASSERT_FALSE(filter_.KeepField(context_, timeline_field));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EveryStartEventType,
+    FilterCookiesFieldsTest,
+    testing::Values(
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::kActualSurfaceFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kExpectedDisplayFrameStartFieldNumber,
+        protos::pbzero::FrameTimelineEvent::
+            kActualDisplayFrameStartFieldNumber));
+
+TEST_F(FilterCookiesFieldsTest, IncludeIncludedEndCookies) {
+  context_.package_frame_cookies.insert(kCookieA);
+
+  auto packet = CreateFrameEnd(kTimestampA, kCookieA);
+  auto timeline_field = ExtractTimelineEvent(packet);
+
+  ASSERT_TRUE(filter_.KeepField(context_, timeline_field));
+}
+
+TEST_F(FilterCookiesFieldsTest, ExcludeMissingEndCookies) {
+  auto packet = CreateFrameEnd(kTimestampA, kCookieA);
+  auto timeline_field = ExtractTimelineEvent(packet);
+
+  ASSERT_FALSE(filter_.KeepField(context_, timeline_field));
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/build_timeline.cc b/src/trace_redaction/collect_timeline_events.cc
similarity index 82%
rename from src/trace_redaction/build_timeline.cc
rename to src/trace_redaction/collect_timeline_events.cc
index e94ddaa..aac9752 100644
--- a/src/trace_redaction/build_timeline.cc
+++ b/src/trace_redaction/collect_timeline_events.cc
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_timeline_events.h"
 
-#include "perfetto/ext/base/status_or.h"
 #include "src/trace_redaction/process_thread_timeline.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
@@ -109,16 +108,21 @@
 
 }  // namespace
 
-base::StatusOr<CollectPrimitive::ContinueCollection> BuildTimeline::Collect(
-    const TracePacket::Decoder& packet,
-    Context* context) const {
-  // TODO(vaage): This should only be true on the first call. However, that
-  // means a branch is called N times when N-1 times it will be false. This may
-  // be common across Collect primitives. Having a "begin" and "end" end-points.
-  if (!context->timeline) {
-    context->timeline = std::make_unique<ProcessThreadTimeline>();
+base::Status CollectTimelineEvents::Begin(Context* context) const {
+  // This primitive is artifically limited to owning the timeline. In practice
+  // there is no reason why multiple primitives could contribute to the
+  // timeline.
+  if (context->timeline) {
+    return base::ErrStatus(
+        "CollectTimelineEvents: timeline was already initialized");
   }
 
+  context->timeline = std::make_unique<ProcessThreadTimeline>();
+  return base::OkStatus();
+}
+
+base::Status CollectTimelineEvents::Collect(const TracePacket::Decoder& packet,
+                                            Context* context) const {
   // Unlike ftrace events, process trees do not provide per-process or
   // per-thread timing information. The packet has timestamp and the process
   // tree has collection_end_timestamp (collection_end_timestamp > timestamp).
@@ -129,16 +133,21 @@
     AppendEvents(packet.timestamp(),
                  ProcessTree::Decoder(packet.process_tree()),
                  context->timeline.get());
-    return ContinueCollection::kNextPacket;
   }
 
   if (packet.has_ftrace_events()) {
     AppendEvents(FtraceEventBundle::Decoder(packet.ftrace_events()),
                  context->timeline.get());
-    return ContinueCollection::kNextPacket;
   }
 
-  return ContinueCollection::kNextPacket;
+  return base::OkStatus();
+}
+
+base::Status CollectTimelineEvents::End(Context* context) const {
+  // Sort must be called in order to read from the timeline. If any more events
+  // are added after this, then sort will need to be called again.
+  context->timeline->Sort();
+  return base::OkStatus();
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/build_timeline.h b/src/trace_redaction/collect_timeline_events.h
similarity index 66%
rename from src/trace_redaction/build_timeline.h
rename to src/trace_redaction/collect_timeline_events.h
index 49b92bf..27aa337 100644
--- a/src/trace_redaction/build_timeline.h
+++ b/src/trace_redaction/collect_timeline_events.h
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-#ifndef SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
-#define SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
+#ifndef SRC_TRACE_REDACTION_COLLECT_TIMELINE_EVENTS_H_
+#define SRC_TRACE_REDACTION_COLLECT_TIMELINE_EVENTS_H_
 
-#include "perfetto/ext/base/status_or.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
@@ -26,13 +25,16 @@
 
 // Creates events from process_tree, task_newtask, and sched_process_free
 // packets and stores them in a timeline.
-class BuildTimeline : public CollectPrimitive {
+class CollectTimelineEvents : public CollectPrimitive {
  public:
-  base::StatusOr<ContinueCollection> Collect(
-      const protos::pbzero::TracePacket::Decoder& packet,
-      Context* context) const override;
+  base::Status Begin(Context* context) const override;
+
+  base::Status Collect(const protos::pbzero::TracePacket::Decoder& packet,
+                       Context* context) const override;
+
+  base::Status End(Context* context) const override;
 };
 
 }  // namespace perfetto::trace_redaction
 
-#endif  // SRC_TRACE_REDACTION_BUILD_TIMELINE_H_
+#endif  // SRC_TRACE_REDACTION_COLLECT_TIMELINE_EVENTS_H_
diff --git a/src/trace_redaction/build_timeline_unittest.cc b/src/trace_redaction/collect_timeline_events_unittest.cc
similarity index 71%
rename from src/trace_redaction/build_timeline_unittest.cc
rename to src/trace_redaction/collect_timeline_events_unittest.cc
index 39eb877..ecf0905 100644
--- a/src/trace_redaction/build_timeline_unittest.cc
+++ b/src/trace_redaction/collect_timeline_events_unittest.cc
@@ -18,7 +18,7 @@
 #include <string>
 
 #include "src/base/test/status_matchers.h"
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 #include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
@@ -87,11 +87,11 @@
 
 }  // namespace
 
-class BuildTimelineTest : public testing::Test,
-                          public testing::WithParamInterface<TestParams> {
+class CollectTimelineEventsTest
+    : public testing::Test,
+      public testing::WithParamInterface<TestParams> {
  protected:
-  base::StatusOr<CollectPrimitive::ContinueCollection> PushProcessTreePacket(
-      uint64_t timestamp) {
+  std::string CreateProcessTreePacket(uint64_t timestamp) {
     protos::gen::TracePacket packet;
     packet.set_trusted_uid(9999);
     packet.set_timestamp(timestamp);
@@ -119,13 +119,10 @@
 
     process_tree->set_collection_end_timestamp(timestamp);
 
-    std::string packet_str = packet.SerializeAsString();
-    return build_.Collect(protos::pbzero::TracePacket::Decoder(packet_str),
-                          &context_);
+    return packet.SerializeAsString();
   }
 
-  base::StatusOr<CollectPrimitive::ContinueCollection>
-  PushSchedProcessFreePacket(uint64_t timestamp) {
+  std::string CreateSchedProcessFreePacket(uint64_t timestamp) {
     protos::gen::TracePacket packet;
 
     packet.set_trusted_uid(9999);
@@ -144,33 +141,39 @@
     process_free->set_pid(kUnityTid);
     process_free->set_prio(120);
 
-    std::string packet_str = packet.SerializeAsString();
-    return build_.Collect(protos::pbzero::TracePacket::Decoder(packet_str),
-                          &context_);
+    return packet.SerializeAsString();
   }
-
-  BuildTimeline build_;
-  Context context_;
 };
 
-class BuildTimelineWithProcessTree : public BuildTimelineTest {};
+class CollectTimelineEventsWithProcessTree : public CollectTimelineEventsTest {
+};
 
-TEST_P(BuildTimelineWithProcessTree, FindsOpenSpans) {
+TEST_P(CollectTimelineEventsWithProcessTree, FindsOpenSpans) {
   auto params = GetParam();
 
-  auto result = PushProcessTreePacket(kProcessTreeTimestamp);
-  ASSERT_OK(result) << result.status().message();
+  auto packet_str = CreateProcessTreePacket(kProcessTreeTimestamp);
 
-  context_.timeline->Sort();
+  protos::pbzero::TracePacket::Decoder packet(packet_str);
 
-  auto slice = context_.timeline->Search(params.ts(), params.pid());
+  Context context;
+  CollectTimelineEvents collector;
+  auto begin_status = collector.Begin(&context);
+  ASSERT_OK(begin_status) << begin_status.message();
+
+  auto packet_status = collector.Collect(packet, &context);
+  ASSERT_OK(packet_status) << packet_status.message();
+
+  auto end_status = collector.End(&context);
+  ASSERT_OK(end_status) << end_status.message();
+
+  auto slice = context.timeline->Search(params.ts(), params.pid());
   ASSERT_EQ(slice.pid, params.pid());
   ASSERT_EQ(slice.uid, params.uid());
 }
 
 INSTANTIATE_TEST_SUITE_P(
     AcrossWholeTimeline,
-    BuildTimelineWithProcessTree,
+    CollectTimelineEventsWithProcessTree,
     testing::Values(
         // Before the processes/threads existed.
         TestParams(0, kZygotePid, kNoPackage),
@@ -187,28 +190,41 @@
         TestParams(kProcessTreeTimestamp + 1, kUnityPid, kUnityPackage),
         TestParams(kProcessTreeTimestamp + 1, kUnityTid, kUnityPackage)));
 
-// Assumes all BuildTimelineWithProcessTree tests pass.
-class BuildTimelineWithFreeProcess : public BuildTimelineTest {};
+// Assumes all CollectTimelineEventsWithProcessTree tests pass.
+class CollectTimelineEventsWithFreeProcess : public CollectTimelineEventsTest {
+};
 
-TEST_P(BuildTimelineWithFreeProcess, FindsClosedSpans) {
+TEST_P(CollectTimelineEventsWithFreeProcess, FindsClosedSpans) {
   auto params = GetParam();
 
-  auto result = PushProcessTreePacket(kProcessTreeTimestamp);
-  ASSERT_OK(result) << result.status().message();
+  auto packet_1_str = CreateProcessTreePacket(kProcessTreeTimestamp);
+  auto packet_2_str = CreateSchedProcessFreePacket(kThreadFreeTimestamp);
 
-  result = PushSchedProcessFreePacket(kThreadFreeTimestamp);
-  ASSERT_OK(result) << result.status().message();
+  protos::pbzero::TracePacket::Decoder packet_1(packet_1_str);
+  protos::pbzero::TracePacket::Decoder packet_2(packet_2_str);
 
-  context_.timeline->Sort();
+  Context context;
+  CollectTimelineEvents collector;
+  auto begin_status = collector.Begin(&context);
+  ASSERT_OK(begin_status) << begin_status.message();
 
-  auto slice = context_.timeline->Search(params.ts(), params.pid());
+  auto packet_1_status = collector.Collect(packet_1, &context);
+  ASSERT_OK(packet_1_status) << packet_1_status.message();
+
+  auto packet_2_status = collector.Collect(packet_2, &context);
+  ASSERT_OK(packet_2_status) << packet_2_status.message();
+
+  auto end_status = collector.End(&context);
+  ASSERT_OK(end_status) << end_status.message();
+
+  auto slice = context.timeline->Search(params.ts(), params.pid());
   ASSERT_EQ(slice.pid, params.pid());
   ASSERT_EQ(slice.uid, params.uid());
 }
 
 INSTANTIATE_TEST_SUITE_P(
     AcrossWholeTimeline,
-    BuildTimelineWithFreeProcess,
+    CollectTimelineEventsWithFreeProcess,
     testing::Values(
         TestParams(kThreadFreeTimestamp - 1, kZygotePid, kNoPackage),
         TestParams(kThreadFreeTimestamp - 1, kUnityPid, kUnityPackage),
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
index 89946ae..e53770c 100644
--- a/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
+++ b/src/trace_redaction/filter_ftrace_using_allowlist_integrationtest.cc
@@ -18,14 +18,11 @@
 #include <string>
 
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/file_utils.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/filter_ftrace_using_allowlist.h"
 #include "src/trace_redaction/populate_allow_lists.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
-#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -36,39 +33,15 @@
 
 namespace perfetto::trace_redaction {
 
-class FilterFtraceUsingAllowlistTest : public testing::Test {
+class FilterFtraceUsingAllowlistTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
-    redactor_.emplace_build<PopulateAllowlists>();
-
-    auto* scrub_ftrace_events =
-        redactor_.emplace_transform<ScrubFtraceEvents>();
-    scrub_ftrace_events->emplace_back<FilterFtraceUsingAllowlist>();
-
-    src_trace_ =
-        base::GetTestDataPath("test/data/trace-redaction-general.pftrace");
-
-    dest_trace_ = tmp_dir_.AbsolutePath("dst.pftrace");
-  }
-
-  base::Status Redact() {
-    auto status = redactor_.Redact(src_trace_, dest_trace_, &context_);
-
-    // If redaction failed, the redactor should not have written the file to
-    // disk.
-    if (status.ok()) {
-      tmp_dir_.TrackFile("dst.pftrace");
-    }
-
-    return status;
-  }
-
-  base::StatusOr<std::string> LoadOriginal() const {
-    return ReadRawTrace(src_trace_);
-  }
-
-  base::StatusOr<std::string> LoadRedacted() const {
-    return ReadRawTrace(dest_trace_);
+    trace_redactor()->emplace_build<PopulateAllowlists>();
+    trace_redactor()
+        ->emplace_transform<ScrubFtraceEvents>()
+        ->emplace_back<FilterFtraceUsingAllowlist>();
   }
 
   // Parse the given buffer and gather field ids from across all events. This
@@ -100,25 +73,6 @@
 
     return event_ids;
   }
-
- 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_;
 };
 
 // This is not a test of FilterFtraceUsingAllowlist, but instead verifies of the
diff --git a/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc b/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
index 6f6672a..2b45043 100644
--- a/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
+++ b/src/trace_redaction/filter_ftrace_using_allowlist_unittest.cc
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-#include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/filter_ftrace_using_allowlist.h"
-#include "protos/perfetto/trace/ftrace/power.gen.h"
 #include "src/base/test/status_matchers.h"
+#include "src/trace_redaction/scrub_ftrace_events.h"
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/power.gen.h"
 #include "protos/perfetto/trace/ftrace/task.gen.h"
 #include "protos/perfetto/trace/ps/process_tree.gen.h"
 #include "protos/perfetto/trace/trace_packet.gen.h"
diff --git a/src/trace_redaction/filter_packet_using_allowlist.cc b/src/trace_redaction/filter_packet_using_allowlist.cc
index 2a900d4..b1a4dc4 100644
--- a/src/trace_redaction/filter_packet_using_allowlist.cc
+++ b/src/trace_redaction/filter_packet_using_allowlist.cc
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-#include <string>
-
 #include "perfetto/base/status.h"
 #include "perfetto/protozero/field.h"
 #include "src/trace_redaction/filter_packet_using_allowlist.h"
@@ -26,31 +24,17 @@
 base::Status FilterPacketUsingAllowlist::VerifyContext(
     const Context& context) const {
   if (context.trace_packet_allow_list.empty()) {
-    return base::ErrStatus("Cannot scrub trace packets, missing allow-list.");
+    return base::ErrStatus("FilterPacketUsingAllowlist: missing allow-list.");
   }
 
   return base::OkStatus();
 }
 
-bool FilterPacketUsingAllowlist::KeepPacket(const Context& context,
-                                            const std::string& bytes) const {
+bool FilterPacketUsingAllowlist::KeepField(
+    const Context& context,
+    const protozero::Field& field) const {
   PERFETTO_DCHECK(!context.trace_packet_allow_list.empty());
-
-  const auto& allow_list = context.trace_packet_allow_list;
-
-  protozero::ProtoDecoder decoder(bytes);
-
-  // A packet should only have one data type (proto oneof), but there are other
-  // values in the packet (e.g. timestamp). If one field is in the allowlist,
-  // then allow the whole trace packet.
-  for (auto field = decoder.ReadField(); field.valid();
-       field = decoder.ReadField()) {
-    if (allow_list.count(field.id()) != 0) {
-      return true;
-    }
-  }
-
-  return false;
+  return field.valid() && context.trace_packet_allow_list.count(field.id());
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_packet_using_allowlist.h b/src/trace_redaction/filter_packet_using_allowlist.h
index 7caa0ec..9205e94 100644
--- a/src/trace_redaction/filter_packet_using_allowlist.h
+++ b/src/trace_redaction/filter_packet_using_allowlist.h
@@ -31,8 +31,8 @@
  public:
   base::Status VerifyContext(const Context& context) const override;
 
-  bool KeepPacket(const Context& context,
-                  const std::string& bytes) const override;
+  bool KeepField(const Context& context,
+                 const protozero::Field& field) const override;
 };
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_packet_using_allowlist_unittest.cc b/src/trace_redaction/filter_packet_using_allowlist_unittest.cc
index ffdb99f..fad6879 100644
--- a/src/trace_redaction/filter_packet_using_allowlist_unittest.cc
+++ b/src/trace_redaction/filter_packet_using_allowlist_unittest.cc
@@ -14,134 +14,59 @@
  * limitations under the License.
  */
 
-#include <string>
-
-#include "src/base/test/status_matchers.h"
 #include "src/trace_redaction/filter_packet_using_allowlist.h"
-#include "src/trace_redaction/scrub_trace_packet.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
 #include "test/gtest_and_gmock.h"
 
-#include "protos/perfetto/trace/ps/process_tree.gen.h"
-#include "protos/perfetto/trace/trace_packet.gen.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
+// TODO(vaage): These tests were used to test the filter-driver, but these tests
+// no longer do that. A new test suite should be created to test the driver code
+// with the different filters.
 namespace perfetto::trace_redaction {
 
-TEST(FilterPacketUsingAllowlistParamErrorTest, ReturnErrorForNullPacket) {
-  ScrubTracePacket transform_;
-  transform_.emplace_back<FilterPacketUsingAllowlist>();
+namespace {
 
-  // Have something in the allow-list to avoid that error.
+constexpr auto kJustSomeFieldId =
+    protos::pbzero::TracePacket::kProcessTreeFieldNumber;
+
+}  // namespace
+
+TEST(FilterPacketUsingAllowlistParamErrorTest, ReturnsErrorForEmptyAllowlist) {
   Context context;
-  context.trace_packet_allow_list.insert(
-      protos::pbzero::TracePacket::kProcessTreeFieldNumber);
 
-  ASSERT_FALSE(transform_.Transform(context, nullptr).ok());
+  FilterPacketUsingAllowlist filter;
+  auto status = filter.VerifyContext(context);
+
+  ASSERT_FALSE(status.ok()) << status.message();
 }
 
-TEST(FilterPacketUsingAllowlistParamErrorTest, ReturnErrorForEmptyPacket) {
-  ScrubTracePacket transform_;
-  transform_.emplace_back<FilterPacketUsingAllowlist>();
-
-  // Have something in the allow-list to avoid that error.
+TEST(FilterPacketUsingAllowlistParamErrorTest, ReturnsFalseForInvalidField) {
+  // Have something in the allow-list to avoid an error.
   Context context;
-  context.trace_packet_allow_list.insert(
-      protos::pbzero::TracePacket::kProcessTreeFieldNumber);
+  context.trace_packet_allow_list.insert(kJustSomeFieldId);
 
-  std::string packet_str = "";
+  protozero::Field invalid = {};
+  ASSERT_FALSE(invalid.valid());
 
-  ASSERT_FALSE(transform_.Transform(context, &packet_str).ok());
+  FilterPacketUsingAllowlist filter;
+  ASSERT_FALSE(filter.KeepField(context, invalid));
 }
 
-class FilterPacketUsingAllowlistTest : public testing::Test {
- protected:
-  void SetUp() override {
-    transform_.emplace_back<FilterPacketUsingAllowlist>();
-  }
+TEST(FilterPacketUsingAllowlistParamErrorTest, ReturnsFalseForExcludedField) {
+  Context context;
+  context.trace_packet_allow_list.insert(kJustSomeFieldId);
 
-  base::StatusOr<std::string> Redact(const protos::gen::TracePacket& packet) {
-    auto str = packet.SerializeAsString();
-    auto status = transform_.Transform(context_, &str);
+  protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
+  packet->set_timestamp(123456789);
 
-    if (status.ok()) {
-      return str;
-    }
+  auto buffer = packet.SerializeAsString();
 
-    return status;
-  }
+  protozero::ProtoDecoder decoder(buffer);
+  protozero::Field field = decoder.FindField(kJustSomeFieldId);
 
-  Context context_;
-
- private:
-  ScrubTracePacket transform_;
-};
-
-TEST_F(FilterPacketUsingAllowlistTest, ReturnErrorForEmptyAllowList) {
-  // The context will have no allow-list entries. ScrubTracePacket should fail.
-
-  protos::gen::TracePacket packet;
-
-  auto status = Redact(packet);
-  ASSERT_FALSE(status.ok()) << status.status().c_message();
-}
-
-// The whole packet should be dropped (cleared) when it has a data type not
-// included in the allow-list.
-TEST_F(FilterPacketUsingAllowlistTest, DropsOutsiderPacketType) {
-  protos::gen::TracePacket packet;
-  packet.set_timestamp(1234);
-  packet.mutable_android_camera_frame_event();  // Creates and sets data.
-
-  // Populate the allow-list with something that doesn't match the data in the
-  // packet.
-  context_.trace_packet_allow_list.insert(
-      protos::pbzero::TracePacket::kProcessTreeFieldNumber);
-
-  auto status = Redact(packet);
-  ASSERT_OK(status) << status.status().c_message();
-
-  ASSERT_TRUE(status->empty());
-}
-
-// Typically a trace packet should always have a data type (e.g. ProcessTree),
-// but it is possible that another transformation has cleared that data. If
-// that's the case, this primitive should treat it as an outsider.
-TEST_F(FilterPacketUsingAllowlistTest, DropsPacketsWithNoType) {
-  protos::gen::TracePacket packet;
-  packet.set_timestamp(1234);
-
-  std::string packet_str = packet.SerializeAsString();
-  ASSERT_GT(packet_str.size(), 0u);
-
-  context_.trace_packet_allow_list.insert(
-      protos::pbzero::TracePacket::kProcessTreeFieldNumber);
-
-  auto status = Redact(packet);
-  ASSERT_OK(status) << status.status().c_message();
-
-  ASSERT_TRUE(status->empty());
-}
-
-// A packet should not change (at all) if it's in the allow-list.
-TEST_F(FilterPacketUsingAllowlistTest, SkipsAllowedPacket) {
-  protos::gen::TracePacket packet;
-  packet.set_timestamp(1234);
-
-  // Add a process tree to the packet. Process trees are in the allow-list.
-  auto* process = packet.mutable_process_tree()->add_processes();
-  process->set_uid(0);
-  process->set_ppid(3);
-  process->set_pid(7);
-
-  context_.trace_packet_allow_list.insert(
-      protos::pbzero::TracePacket::kProcessTreeFieldNumber);
-
-  auto status = Redact(packet);
-  ASSERT_OK(status) << status.status().c_message();
-
-  // The transform shouldn't have changed the string, so the string before and
-  // after should match.
-  ASSERT_EQ(*status, packet.SerializeAsString());
+  FilterPacketUsingAllowlist filter;
+  ASSERT_FALSE(filter.KeepField(context, field));
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_sched_waking_events.cc b/src/trace_redaction/filter_sched_waking_events.cc
index aded8fc..e08a9d6 100644
--- a/src/trace_redaction/filter_sched_waking_events.cc
+++ b/src/trace_redaction/filter_sched_waking_events.cc
@@ -16,10 +16,9 @@
 
 #include "src/trace_redaction/filter_sched_waking_events.h"
 
-#include "protos/perfetto/trace/ftrace/sched.pbzero.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"
 
 namespace perfetto::trace_redaction {
 
diff --git a/src/trace_redaction/filter_sched_waking_events_integrationtest.cc b/src/trace_redaction/filter_sched_waking_events_integrationtest.cc
index 08bc991..d86bf74 100644
--- a/src/trace_redaction/filter_sched_waking_events_integrationtest.cc
+++ b/src/trace_redaction/filter_sched_waking_events_integrationtest.cc
@@ -17,15 +17,11 @@
 #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/collect_timeline_events.h"
 #include "src/trace_redaction/filter_sched_waking_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
@@ -53,7 +49,7 @@
  protected:
   void SetUp() override {
     trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
     trace_redactor()->emplace_build<OptimizeTimeline>();
 
     auto* ftrace_filter =
diff --git a/src/trace_redaction/filter_sched_waking_events_unittest.cc b/src/trace_redaction/filter_sched_waking_events_unittest.cc
index 891a86a..9f71113 100644
--- a/src/trace_redaction/filter_sched_waking_events_unittest.cc
+++ b/src/trace_redaction/filter_sched_waking_events_unittest.cc
@@ -14,15 +14,16 @@
  * limitations under the License.
  */
 
-#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
-#include "protos/perfetto/trace/ftrace/sched.gen.h"
-#include "protos/perfetto/trace/trace_packet.gen.h"
-#include "protos/perfetto/trace/trace.gen.h"
 #include "src/trace_redaction/filter_sched_waking_events.h"
 #include "src/trace_redaction/scrub_ftrace_events.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;
diff --git a/src/trace_redaction/filter_task_rename_integrationtest.cc b/src/trace_redaction/filter_task_rename_integrationtest.cc
index babc2ce..14c66dd 100644
--- a/src/trace_redaction/filter_task_rename_integrationtest.cc
+++ b/src/trace_redaction/filter_task_rename_integrationtest.cc
@@ -21,10 +21,10 @@
 
 #include "perfetto/base/status.h"
 #include "src/base/test/status_matchers.h"
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_timeline_events.h"
+#include "src/trace_redaction/filter_task_rename.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
-#include "src/trace_redaction/filter_task_rename.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 #include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "src/trace_redaction/trace_redactor.h"
@@ -56,7 +56,7 @@
     // In order for ScrubTaskRename to work, it needs the timeline. All
     // registered primitives are there to generate the timeline.
     trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
     trace_redactor()->emplace_build<OptimizeTimeline>();
 
     auto scrub_ftrace_events =
diff --git a/src/trace_redaction/find_package_uid.cc b/src/trace_redaction/find_package_uid.cc
index 0c46ff5..7b5f200 100644
--- a/src/trace_redaction/find_package_uid.cc
+++ b/src/trace_redaction/find_package_uid.cc
@@ -15,8 +15,6 @@
  */
 
 #include "src/trace_redaction/find_package_uid.h"
-
-#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
@@ -24,46 +22,63 @@
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto::trace_redaction {
-namespace {
 
-using PackagesList_PackageInfo = protos::pbzero::PackagesList_PackageInfo;
-using PackagesList = protos::pbzero::PackagesList;
-
-}  // namespace
-
-FindPackageUid::FindPackageUid() = default;
-
-FindPackageUid::~FindPackageUid() = default;
-
-base::StatusOr<CollectPrimitive::ContinueCollection> FindPackageUid::Collect(
-    const protos::pbzero::TracePacket_Decoder& packet,
-    Context* context) const {
+base::Status FindPackageUid::Begin(Context* context) const {
   if (context->package_name.empty()) {
     return base::ErrStatus("FindPackageUid: missing package name.");
   }
 
-  // Skip package and move onto the next packet.
-  if (!packet.has_packages_list()) {
-    return ContinueCollection::kNextPacket;
+  if (context->package_uid.has_value()) {
+    return base::ErrStatus("FindPackageUid: package uid already found.");
   }
 
-  const PackagesList::Decoder pkg_list_decoder(packet.packages_list());
+  return base::OkStatus();
+}
 
-  for (auto pkg_it = pkg_list_decoder.packages(); pkg_it; ++pkg_it) {
-    PackagesList_PackageInfo::Decoder pkg_info(*pkg_it);
+base::Status FindPackageUid::Collect(
+    const protos::pbzero::TracePacket::Decoder& packet,
+    Context* context) const {
+  // If a package has been found in a pervious iteration, stop.
+  if (context->package_uid.has_value()) {
+    return base::OkStatus();
+  }
 
-    if (pkg_info.has_name() && pkg_info.has_uid()) {
+  // Skip package and move onto the next packet.
+  if (!packet.has_packages_list()) {
+    return base::OkStatus();
+  }
+
+  protos::pbzero::PackagesList::Decoder packages_list_decoder(
+      packet.packages_list());
+
+  for (auto package = packages_list_decoder.packages(); package; ++package) {
+    protozero::ProtoDecoder package_decoder(*package);
+
+    auto name = package_decoder.FindField(
+        protos::pbzero::PackagesList::PackageInfo::kNameFieldNumber);
+    auto uid = package_decoder.FindField(
+        protos::pbzero::PackagesList::PackageInfo::kUidFieldNumber);
+
+    if (name.valid() && uid.valid()) {
       // Package names should be lowercase, but this check is meant to be more
       // forgiving.
       if (base::StringView(context->package_name)
-              .CaseInsensitiveEq(pkg_info.name())) {
-        context->package_uid = NormalizeUid(pkg_info.uid());
-        return ContinueCollection::kRetire;
+              .CaseInsensitiveEq(name.as_string())) {
+        context->package_uid = NormalizeUid(uid.as_uint64());
+        return base::OkStatus();
       }
     }
   }
 
-  return ContinueCollection::kNextPacket;
+  return base::OkStatus();
+}
+
+base::Status FindPackageUid::End(Context* context) const {
+  if (!context->package_uid.has_value()) {
+    return base::ErrStatus("FindPackageUid: did not find package uid.");
+  }
+
+  return base::OkStatus();
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/find_package_uid.h b/src/trace_redaction/find_package_uid.h
index d43b2af..70c7baf 100644
--- a/src/trace_redaction/find_package_uid.h
+++ b/src/trace_redaction/find_package_uid.h
@@ -17,7 +17,6 @@
 #ifndef SRC_TRACE_REDACTION_FIND_PACKAGE_UID_H_
 #define SRC_TRACE_REDACTION_FIND_PACKAGE_UID_H_
 
-#include "perfetto/ext/base/status_or.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
@@ -30,12 +29,12 @@
 // need to report the failure.
 class FindPackageUid final : public CollectPrimitive {
  public:
-  FindPackageUid();
-  ~FindPackageUid() override;
+  base::Status Begin(Context*) const override;
 
-  base::StatusOr<ContinueCollection> Collect(
-      const protos::pbzero::TracePacket::Decoder& packet,
-      Context* context) const override;
+  base::Status Collect(const protos::pbzero::TracePacket::Decoder& packet,
+                       Context* context) const override;
+
+  base::Status End(Context*) const override;
 };
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/find_package_uid_unittest.cc b/src/trace_redaction/find_package_uid_unittest.cc
index 5f390ab..dd8e44f 100644
--- a/src/trace_redaction/find_package_uid_unittest.cc
+++ b/src/trace_redaction/find_package_uid_unittest.cc
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
-#include "src/trace_redaction/find_package_uid.h"
-
 #include <cstdint>
 #include <string>
 
 #include "src/base/test/status_matchers.h"
+#include "src/trace_redaction/find_package_uid.h"
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/trace/android/packages_list.gen.h"
@@ -187,8 +186,14 @@
 
   const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
 
-  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
-  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kRetire);
+  base::Status status = find.Begin(&context);
+  ASSERT_OK(status) << status.message();
+
+  status = find.Collect(decoder, &context);
+  ASSERT_OK(status) << status.message();
+
+  status = find.End(&context);
+  ASSERT_OK(status) << status.message();
 
   ASSERT_TRUE(context.package_uid.has_value());
   ASSERT_EQ(NormalizeUid(context.package_uid.value()), NormalizeUid(10205));
@@ -204,8 +209,15 @@
 
   const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
 
-  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
-  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kNextPacket);
+  base::Status status = find.Begin(&context);
+  ASSERT_OK(status) << status.message();
+
+  status = find.Collect(decoder, &context);
+  ASSERT_OK(status) << status.message();
+
+  // The should not have been found; End() should return an error.
+  status = find.End(&context);
+  ASSERT_FALSE(status.ok()) << status.message();
 
   ASSERT_FALSE(context.package_uid.has_value());
 }
@@ -220,8 +232,15 @@
 
   const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
 
-  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
-  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kNextPacket);
+  base::Status status = find.Begin(&context);
+  ASSERT_OK(status) << status.message();
+
+  status = find.Collect(decoder, &context);
+  ASSERT_OK(status) << status.message();
+
+  // The should not have been found; End() should return an error.
+  status = find.End(&context);
+  ASSERT_FALSE(status.ok()) << status.message();
 
   ASSERT_FALSE(context.package_uid.has_value());
 }
@@ -230,10 +249,28 @@
   const auto packet = CreatePackageListPacket();
 
   Context context;
+
   const FindPackageUid find;
 
   const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
-  ASSERT_FALSE(find.Collect(decoder, &context).ok());
+
+  base::Status status = find.Begin(&context);
+  ASSERT_FALSE(status.ok()) << status.message();
+}
+
+TEST(FindPackageUidTest, FailsIfUidStartsInitialized) {
+  const auto packet = CreatePackageListPacket();
+
+  Context context;
+  context.package_name = "com.google.android.uvexposurereporter";
+  context.package_uid = 1000;
+
+  const FindPackageUid find;
+
+  const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
+
+  base::Status status = find.Begin(&context);
+  ASSERT_FALSE(status.ok()) << status.message();
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/frame_cookie.h b/src/trace_redaction/frame_cookie.h
new file mode 100644
index 0000000..bd96804
--- /dev/null
+++ b/src/trace_redaction/frame_cookie.h
@@ -0,0 +1,42 @@
+/*
+ * 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_FRAME_COOKIE_H_
+#define SRC_TRACE_REDACTION_FRAME_COOKIE_H_
+
+#include <cstdint>
+
+struct FrameCookie {
+  // The timestamp from the trace packet.
+  uint64_t ts;
+
+  // The cookie value will be found inside of the start event (there are four
+  // different start types). This is the app's pid (main thread id).
+
+  // ExpectedSurfaceFrameStart: pid = app id
+  // ActualSurfaceFrameStart: pid = app id
+
+  // ExpectedDisplayFrameStart: pid = surface flinger
+  // ActualDisplayFrameStart: pid = surface flinger
+  int32_t pid;
+
+  // The cookie value will be found inside of the start event (there are four
+  // different start types). End events use the cookie to connect to the start
+  // event. Therefore end events don't need a pid.
+  int64_t cookie;
+};
+
+#endif  // SRC_TRACE_REDACTION_FRAME_COOKIE_H_
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index 003ad78..2e433c0 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -16,7 +16,8 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_frame_cookies.h"
+#include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/filter_ftrace_using_allowlist.h"
 #include "src/trace_redaction/filter_packet_using_allowlist.h"
 #include "src/trace_redaction/filter_print_events.h"
@@ -34,6 +35,7 @@
 #include "src/trace_redaction/scrub_process_stats.h"
 #include "src/trace_redaction/scrub_process_trees.h"
 #include "src/trace_redaction/scrub_trace_packet.h"
+#include "src/trace_redaction/suspend_resume.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 #include "src/trace_redaction/trace_redactor.h"
 
@@ -47,21 +49,26 @@
 
   // Add all collectors.
   redactor.emplace_collect<FindPackageUid>();
-  redactor.emplace_collect<BuildTimeline>();
+  redactor.emplace_collect<CollectTimelineEvents>();
+  redactor.emplace_collect<CollectFrameCookies>();
 
   // Add all builders.
   redactor.emplace_build<PopulateAllowlists>();
+  redactor.emplace_build<AllowSuspendResume>();
   redactor.emplace_build<OptimizeTimeline>();
+  redactor.emplace_build<ReduceFrameCookies>();
 
   // Add all transforms.
   auto* scrub_packet = redactor.emplace_transform<ScrubTracePacket>();
   scrub_packet->emplace_back<FilterPacketUsingAllowlist>();
+  scrub_packet->emplace_back<FilterFrameEvents>();
 
   auto* scrub_ftrace_events = redactor.emplace_transform<ScrubFtraceEvents>();
   scrub_ftrace_events->emplace_back<FilterFtraceUsingAllowlist>();
   scrub_ftrace_events->emplace_back<FilterPrintEvents>();
   scrub_ftrace_events->emplace_back<FilterSchedWakingEvents>();
   scrub_ftrace_events->emplace_back<FilterTaskRename>();
+  scrub_ftrace_events->emplace_back<FilterSuspendResume>();
 
   // Scrub packets and ftrace events first as they will remove the largest
   // chucks of data from the trace. This will reduce the amount of data that the
diff --git a/src/trace_redaction/optimize_timeline.cc b/src/trace_redaction/optimize_timeline.cc
index f4f09e6..713f7c7 100644
--- a/src/trace_redaction/optimize_timeline.cc
+++ b/src/trace_redaction/optimize_timeline.cc
@@ -24,7 +24,8 @@
 base::Status OptimizeTimeline::Build(Context* context) const {
   if (!context->timeline) {
     return base::ErrStatus(
-        "Cannot optimize a null timeline. Are you missing BuildTimeline or an "
+        "Cannot optimize a null timeline. Are you missing "
+        "CollectTimelineEvents or an "
         "alternative?");
   }
 
diff --git a/src/trace_redaction/populate_allow_lists.cc b/src/trace_redaction/populate_allow_lists.cc
index 7a5b48a..b2c9431 100644
--- a/src/trace_redaction/populate_allow_lists.cc
+++ b/src/trace_redaction/populate_allow_lists.cc
@@ -25,8 +25,30 @@
 namespace perfetto::trace_redaction {
 
 base::Status PopulateAllowlists::Build(Context* context) const {
-  if (!context->trace_packet_allow_list.empty()) {
-    return base::ErrStatus("Trace packet allow-list should be empty.");
+  // These fields are top-level fields that outside the "oneof data" field.
+  std::initializer_list<uint32_t> required_trace_fields = {
+
+      protos::pbzero::TracePacket::kTimestampFieldNumber,
+      protos::pbzero::TracePacket::kTimestampClockIdFieldNumber,
+      protos::pbzero::TracePacket::kTrustedUidFieldNumber,
+      protos::pbzero::TracePacket::kTrustedPacketSequenceIdFieldNumber,
+      protos::pbzero::TracePacket::kTrustedPidFieldNumber,
+      protos::pbzero::TracePacket::kInternedDataFieldNumber,
+      protos::pbzero::TracePacket::kSequenceFlagsFieldNumber,
+
+      // DEPRECATED. Moved to SequenceFlags::SEQ_INCREMENTAL_STATE_CLEARED. So
+      // there is no reason to include it.
+      //
+      // protos::pbzero::TracePacket::incremental_state_cleared
+
+      protos::pbzero::TracePacket::kTracePacketDefaultsFieldNumber,
+      protos::pbzero::TracePacket::kPreviousPacketDroppedFieldNumber,
+      protos::pbzero::TracePacket::kFirstPacketOnSequenceFieldNumber,
+      protos::pbzero::TracePacket::kMachineIdFieldNumber,
+  };
+
+  for (auto item : required_trace_fields) {
+    context->trace_packet_allow_list.insert(item);
   }
 
   // TRACE PACKET NOTES
@@ -37,8 +59,7 @@
   //      constraints around keys or values, making fine-grain redaction
   //      difficult. Because this packet's value has no measurable, the safest
   //      option to drop the whole packet.
-
-  context->trace_packet_allow_list = {
+  std::initializer_list<uint32_t> trace_packets = {
       protos::pbzero::TracePacket::kProcessTreeFieldNumber,
       protos::pbzero::TracePacket::kProcessStatsFieldNumber,
       protos::pbzero::TracePacket::kClockSnapshotFieldNumber,
@@ -59,13 +80,16 @@
       protos::pbzero::TracePacket::kPackagesListFieldNumber,
   };
 
-  context->ftrace_packet_allow_list = {
+  for (auto item : trace_packets) {
+    context->trace_packet_allow_list.insert(item);
+  }
+
+  std::initializer_list<uint32_t> ftrace_events = {
       protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber,
       protos::pbzero::FtraceEvent::kCpuFrequencyFieldNumber,
       protos::pbzero::FtraceEvent::kCpuIdleFieldNumber,
       protos::pbzero::FtraceEvent::kSchedBlockedReasonFieldNumber,
       protos::pbzero::FtraceEvent::kSchedWakingFieldNumber,
-      protos::pbzero::FtraceEvent::kSuspendResumeFieldNumber,
       protos::pbzero::FtraceEvent::kTaskNewtaskFieldNumber,
       protos::pbzero::FtraceEvent::kTaskRenameFieldNumber,
       protos::pbzero::FtraceEvent::kSchedProcessFreeFieldNumber,
@@ -80,21 +104,9 @@
       protos::pbzero::FtraceEvent::kPrintFieldNumber,
   };
 
-  // TODO: Some ftrace fields should be retained, but they carry too much risk
-  // without additional redaction. This list should be configured in a build
-  // primitive so that they can be optionally included.
-  //
-  // protos::pbzero::FtraceEvent::kPrintFieldNumber,
-  //
-  // TODO: Some fields will create new packets (e.g. binder calls may create
-  // new spans. This is currently not supported (generated packets still
-  // need to be redacted).
-  //
-  // protos::pbzero::FtraceEvent::kBinderTransactionFieldNumber,
-  // protos::pbzero::FtraceEvent::kBinderTransactionReceivedFieldNumber,
-  // protos::pbzero::FtraceEvent::kBinderSetPriorityFieldNumber,
-  // protos::pbzero::FtraceEvent::kBinderLockedFieldNumber,
-  // protos::pbzero::FtraceEvent::kBinderUnlockFieldNumber,
+  for (auto item : ftrace_events) {
+    context->ftrace_packet_allow_list.insert(item);
+  }
 
   return base::OkStatus();
 }
diff --git a/src/trace_redaction/prune_package_list.cc b/src/trace_redaction/prune_package_list.cc
index de6331b..2dab1fc 100644
--- a/src/trace_redaction/prune_package_list.cc
+++ b/src/trace_redaction/prune_package_list.cc
@@ -20,11 +20,11 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
-
 #include "perfetto/protozero/scattered_heap_buffer.h"
-#include "protos/perfetto/trace/android/packages_list.pbzero.h"
 #include "src/trace_redaction/proto_util.h"
 
+#include "protos/perfetto/trace/android/packages_list.pbzero.h"
+
 namespace perfetto::trace_redaction {
 namespace {
 
diff --git a/src/trace_redaction/prune_package_list_integrationtest.cc b/src/trace_redaction/prune_package_list_integrationtest.cc
new file mode 100644
index 0000000..8fd056a
--- /dev/null
+++ b/src/trace_redaction/prune_package_list_integrationtest.cc
@@ -0,0 +1,162 @@
+/*
+ * 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 "src/base/test/status_matchers.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/prune_package_list.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/android/packages_list.gen.h"
+#include "protos/perfetto/trace/android/packages_list.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+// Set the package name to "just some package name". If a specific package name
+// is needed, the test it should overwrite this value.
+constexpr std::string_view kPackageName =
+    "com.Unity.com.unity.multiplayer.samples.coop";
+constexpr uint64_t kPackageUid = 10252;
+
+}  // namespace
+
+class PrunePackageListIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
+ protected:
+  void SetUp() override {
+    context()->package_name = kPackageName;
+
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_transform<PrunePackageList>();
+  }
+
+  std::vector<protos::gen::PackagesList::PackageInfo> GetPackageInfo(
+      const protos::pbzero::Trace::Decoder& trace) const {
+    std::vector<protos::gen::PackagesList::PackageInfo> packages;
+
+    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
+      protos::pbzero::TracePacket::Decoder packet(*packet_it);
+
+      if (!packet.has_packages_list()) {
+        continue;
+      }
+
+      protos::pbzero::PackagesList::Decoder list(packet.packages_list());
+
+      for (auto info = list.packages(); info; ++info) {
+        auto& item = packages.emplace_back();
+        item.ParseFromArray(info->data(), info->size());
+      }
+    }
+
+    return packages;
+  }
+
+  std::vector<std::string> GetPackageNames(
+      const protos::pbzero::Trace::Decoder& trace) const {
+    std::vector<std::string> names;
+
+    for (const auto& package : GetPackageInfo(trace)) {
+      if (package.has_name()) {
+        names.push_back(package.name());
+      }
+    }
+
+    return names;
+  }
+};
+
+// It is possible for two packages_list to appear in the trace. The
+// find_package_uid will stop after the first one is found. Package uids are
+// appear as n * 1,000,000 where n is some integer. It is also possible for two
+// packages_list to contain copies of each other - for example
+// "com.Unity.com.unity.multiplayer.samples.coop" appears in both packages_list.
+TEST_F(PrunePackageListIntegrationTest, FindsPackageAndFiltersPackageList) {
+  auto result = Redact();
+  ASSERT_OK(result) << result.message();
+
+  auto after_raw_trace = LoadRedacted();
+  ASSERT_OK(after_raw_trace) << after_raw_trace.status().message();
+
+  ASSERT_TRUE(context()->package_uid.has_value());
+  ASSERT_EQ(NormalizeUid(context()->package_uid.value()),
+            NormalizeUid(kPackageUid));
+
+  protos::pbzero::Trace::Decoder redacted_trace(after_raw_trace.value());
+  auto packages = GetPackageInfo(redacted_trace);
+
+  ASSERT_EQ(packages.size(), 2u);
+
+  for (const auto& package : packages) {
+    ASSERT_TRUE(package.has_name());
+    ASSERT_EQ(package.name(), kPackageName);
+
+    ASSERT_TRUE(package.has_uid());
+    ASSERT_EQ(NormalizeUid(package.uid()), NormalizeUid(kPackageUid));
+  }
+}
+
+// It is possible for multiple packages to share a uid. The names will appears
+// across multiple package lists. The only time the package name appears is in
+// the package list, so there is no way to differentiate these packages (only
+// the uid is used later), so each entry should remain.
+TEST_F(PrunePackageListIntegrationTest, RetainsAllInstancesOfUid) {
+  context()->package_name = "com.google.android.networkstack.tethering";
+
+  auto result = Redact();
+  ASSERT_OK(result) << result.message();
+
+  auto after_raw_trace = LoadRedacted();
+  ASSERT_OK(after_raw_trace) << after_raw_trace.status().message();
+
+  protos::pbzero::Trace::Decoder redacted_trace(after_raw_trace.value());
+  auto package_names = GetPackageNames(redacted_trace);
+
+  std::vector<std::string> expected_package_names = {
+      "com.google.android.cellbroadcastservice",
+      "com.google.android.cellbroadcastservice",
+      "com.google.android.networkstack",
+      "com.google.android.networkstack",
+      "com.google.android.networkstack.permissionconfig",
+      "com.google.android.networkstack.permissionconfig",
+      "com.google.android.networkstack.tethering",
+      "com.google.android.networkstack.tethering",
+  };
+
+  // Sort to make compare possible.
+  std::sort(expected_package_names.begin(), expected_package_names.end());
+  std::sort(package_names.begin(), package_names.end());
+
+  ASSERT_TRUE(std::equal(package_names.begin(), package_names.end(),
+                         expected_package_names.begin(),
+                         expected_package_names.end()));
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
index e73960c..2f85efe 100644
--- a/src/trace_redaction/redact_sched_switch.cc
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -16,8 +16,6 @@
 
 #include "src/trace_redaction/redact_sched_switch.h"
 
-#include <string>
-
 #include "src/trace_redaction/proto_util.h"
 
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
@@ -59,11 +57,11 @@
     protozero::ConstBytes bytes,
     protos::pbzero::FtraceEvent* event_message) const {
   if (!context.package_uid.has_value()) {
-    return base::Status("RedactSchedSwitch: missing package uid");
+    return base::ErrStatus("RedactSchedSwitch: missing package uid");
   }
 
   if (!context.timeline) {
-    return base::Status("RedactSchedSwitch: missing timeline");
+    return base::ErrStatus("RedactSchedSwitch: missing timeline");
   }
 
   protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(bytes);
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
index 08f6cc8..3b3523f 100644
--- a/src/trace_redaction/redact_sched_switch_integrationtest.cc
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -20,7 +20,7 @@
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "src/base/test/status_matchers.h"
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/redact_sched_switch.h"
@@ -43,7 +43,7 @@
  protected:
   void SetUp() override {
     trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
     trace_redactor()->emplace_build<OptimizeTimeline>();
 
     auto* ftrace_event_redactions =
diff --git a/src/trace_redaction/redact_task_newtask.cc b/src/trace_redaction/redact_task_newtask.cc
index ed50fbb..cdd34d7 100644
--- a/src/trace_redaction/redact_task_newtask.cc
+++ b/src/trace_redaction/redact_task_newtask.cc
@@ -56,11 +56,11 @@
     protozero::ConstBytes bytes,
     protos::pbzero::FtraceEvent* event_message) const {
   if (!context.package_uid.has_value()) {
-    return base::Status("RedactTaskNewTask: missing package uid");
+    return base::ErrStatus("RedactTaskNewTask: missing package uid");
   }
 
   if (!context.timeline) {
-    return base::Status("RedactTaskNewTask: missing timeline");
+    return base::ErrStatus("RedactTaskNewTask: missing timeline");
   }
 
   // There must be a pid. If not, the message is meaningless and can be dropped.
diff --git a/src/trace_redaction/scrub_ftrace_events.cc b/src/trace_redaction/scrub_ftrace_events.cc
index c3d1abf..89d0b8a 100644
--- a/src/trace_redaction/scrub_ftrace_events.cc
+++ b/src/trace_redaction/scrub_ftrace_events.cc
@@ -43,7 +43,7 @@
 base::Status ScrubFtraceEvents::Transform(const Context& context,
                                           std::string* packet) const {
   if (packet == nullptr || packet->empty()) {
-    return base::ErrStatus("FilterPrintEvents: null or empty packet.");
+    return base::ErrStatus("ScrubFtraceEvents: null or empty packet.");
   }
 
   for (const auto& filter : filters_) {
diff --git a/src/trace_redaction/scrub_ftrace_events_integrationtest.cc b/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
index ec026d1..50853c6 100644
--- a/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
+++ b/src/trace_redaction/scrub_ftrace_events_integrationtest.cc
@@ -14,64 +14,37 @@
  * limitations under the License.
  */
 
-#include <string>
-#include <string_view>
 #include <vector>
 
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/file_utils.h"
 #include "src/base/test/status_matchers.h"
-#include "src/base/test/utils.h"
-#include "src/trace_redaction/filter_ftrace_using_allowlist.h"
 #include "src/trace_redaction/scrub_ftrace_events.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
+#include "src/trace_redaction/trace_redaction_integration_fixture.h"
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/trace//ftrace/ftrace_event.pbzero.h"
-#include "protos/perfetto/trace/android/packages_list.pbzero.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto::trace_redaction {
 
-namespace {
-using FtraceEvent = protos::pbzero::FtraceEvent;
-using PackagesList = protos::pbzero::PackagesList;
-using PackageInfo = protos::pbzero::PackagesList::PackageInfo;
-using Trace = protos::pbzero::Trace;
-using TracePacket = protos::pbzero::TracePacket;
-
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
-
 // Runs ScrubFtraceEvents over an actual trace, verifying packet integrity when
 // fields are removed.
-class ScrubFtraceEventsIntegrationTest : public testing::Test {
+class ScrubFtraceEventsIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
  public:
   ScrubFtraceEventsIntegrationTest() = default;
   ~ScrubFtraceEventsIntegrationTest() override = default;
 
  protected:
   void SetUp() override {
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-    context_.ftrace_packet_allow_list.insert(
+    context()->ftrace_packet_allow_list.insert(
         protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
-  }
 
-  std::string src_trace_;
-
-  Context context_;  // Used for allowlist.
-  ScrubFtraceEvents transform_;
-
-  static base::StatusOr<std::string> ReadRawTrace(const std::string& path) {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
+    trace_redactor()->emplace_transform<ScrubFtraceEvents>();
   }
 
   // Gets spans for `event` messages that contain `sched_switch` messages.
@@ -138,23 +111,34 @@
 };
 
 TEST_F(ScrubFtraceEventsIntegrationTest, FindsPackageAndFiltersPackageList) {
-  const auto& src_file = src_trace_;
+  auto redacted = Redact();
+  ASSERT_OK(redacted) << redacted.message();
 
-  auto raw_src_trace = ReadRawTrace(src_file);
-  ASSERT_OK(raw_src_trace);
+  // Load source.
+  auto before_raw_trace = LoadOriginal();
+  ASSERT_OK(before_raw_trace) << before_raw_trace.status().message();
+  protos::pbzero::Trace::Decoder before_trace(before_raw_trace.value());
+  auto before_it = before_trace.packet();
 
-  protos::pbzero::Trace::Decoder source_trace(raw_src_trace.value());
+  // Load redacted.
+  auto after_raw_trace = LoadRedacted();
+  ASSERT_OK(after_raw_trace) << after_raw_trace.status().message();
+  protos::pbzero::Trace::Decoder after_trace(after_raw_trace.value());
+  auto after_it = after_trace.packet();
 
-  for (auto packet_it = source_trace.packet(); packet_it; ++packet_it) {
-    auto packet = packet_it->as_std_string();
-    ASSERT_OK(transform_.Transform(context_, &packet));
+  while (before_it && after_it) {
+    protos::pbzero::TracePacket::Decoder before_packet(*before_it);
+    protos::pbzero::TracePacket::Decoder after_packet(*after_it);
 
-    protos::pbzero::TracePacket::Decoder left_packet(*packet_it);
-    protos::pbzero::TracePacket::Decoder right_packet(packet);
+    ComparePackets(std::move(before_packet), std::move(after_packet));
 
-    ComparePackets(std::move(left_packet), std::move(right_packet));
+    ++before_it;
+    ++after_it;
   }
+
+  // Both should be at the end.
+  ASSERT_FALSE(before_it);
+  ASSERT_FALSE(after_it);
 }
 
-}  // namespace
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/scrub_process_stats.cc b/src/trace_redaction/scrub_process_stats.cc
index 991c02f..19d4048 100644
--- a/src/trace_redaction/scrub_process_stats.cc
+++ b/src/trace_redaction/scrub_process_stats.cc
@@ -21,10 +21,11 @@
 #include "perfetto/base/status.h"
 #include "perfetto/protozero/field.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
-#include "protos/perfetto/trace/ps/process_stats.pbzero.h"
 #include "src/trace_redaction/proto_util.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
+#include "protos/perfetto/trace/ps/process_stats.pbzero.h"
+
 namespace perfetto::trace_redaction {
 
 base::Status ScrubProcessStats::Transform(const Context& context,
diff --git a/src/trace_redaction/scrub_process_stats_integrationtest.cc b/src/trace_redaction/scrub_process_stats_integrationtest.cc
index 01c61b1..2b081e5 100644
--- a/src/trace_redaction/scrub_process_stats_integrationtest.cc
+++ b/src/trace_redaction/scrub_process_stats_integrationtest.cc
@@ -19,7 +19,7 @@
 
 #include "perfetto/base/status.h"
 #include "src/base/test/status_matchers.h"
-#include "src/trace_redaction/build_timeline.h"
+#include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/scrub_process_stats.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
@@ -37,7 +37,7 @@
                               protected TraceRedactionIntegrationFixure {
  protected:
   void SetUp() override {
-    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
     trace_redactor()->emplace_build<OptimizeTimeline>();
     trace_redactor()->emplace_transform<ScrubProcessStats>();
 
diff --git a/src/trace_redaction/scrub_process_trees.cc b/src/trace_redaction/scrub_process_trees.cc
index 5c54d83..d8baf99 100644
--- a/src/trace_redaction/scrub_process_trees.cc
+++ b/src/trace_redaction/scrub_process_trees.cc
@@ -110,11 +110,11 @@
 base::Status ScrubProcessTrees::Transform(const Context& context,
                                           std::string* packet) const {
   if (!context.package_uid.has_value()) {
-    return base::ErrStatus("Missing package uid.");
+    return base::ErrStatus("ScrubProcessTrees: missing package uid.");
   }
 
   if (context.timeline == nullptr) {
-    return base::ErrStatus("Missing timeline.");
+    return base::ErrStatus("ScrubProcessTrees: missing timeline.");
   }
 
   protozero::ProtoDecoder decoder(*packet);
@@ -126,7 +126,7 @@
   auto timestamp_field = decoder.FindField(kTimestampFieldNumber);
 
   if (!timestamp_field.valid()) {
-    return base::ErrStatus("Could not find timestamp in trace packet");
+    return base::ErrStatus("ScrubProcessTrees: trace packet missing timestamp");
   }
 
   auto timestamp = timestamp_field.as_uint64();
diff --git a/src/trace_redaction/scrub_process_trees_integrationtest.cc b/src/trace_redaction/scrub_process_trees_integrationtest.cc
index 7d13721..58396b2 100644
--- a/src/trace_redaction/scrub_process_trees_integrationtest.cc
+++ b/src/trace_redaction/scrub_process_trees_integrationtest.cc
@@ -18,14 +18,8 @@
 #include <string_view>
 #include <vector>
 
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/file_utils.h"
-#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
-#include "protos/perfetto/trace/trace.pbzero.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/collect_timeline_events.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/optimize_timeline.h"
 #include "src/trace_redaction/scrub_process_trees.h"
@@ -34,6 +28,9 @@
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
+#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+
 namespace perfetto::trace_redaction {
 
 namespace {
@@ -54,13 +51,13 @@
     //
     // OptimizeTimeline depends on:
     //    - FindPackageUid (uses: uid)
-    //    - BuildTimeline  (uses: timeline)
+    //    - CollectTimelineEvents  (uses: timeline)
     //
-    // BuildTimeline depends on.... nothing
+    // CollectTimelineEvents depends on.... nothing
     // FindPackageUid depends on... nothing
 
     trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<BuildTimeline>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
     trace_redactor()->emplace_build<OptimizeTimeline>();
     trace_redactor()->emplace_transform<ScrubProcessTrees>();
 
diff --git a/src/trace_redaction/scrub_trace_packet.cc b/src/trace_redaction/scrub_trace_packet.cc
index 3413040..fd29885 100644
--- a/src/trace_redaction/scrub_trace_packet.cc
+++ b/src/trace_redaction/scrub_trace_packet.cc
@@ -19,38 +19,48 @@
 #include "src/trace_redaction/scrub_trace_packet.h"
 
 #include "perfetto/base/status.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_redaction/proto_util.h"
 
 namespace perfetto::trace_redaction {
 
 TracePacketFilter::~TracePacketFilter() = default;
 
-base::Status ScrubTracePacket::Transform(const Context& context,
-                                         std::string* packet) const {
-  if (packet == nullptr || packet->empty()) {
-    return base::ErrStatus("Cannot scrub null or empty trace packet.");
-  }
-
-  for (const auto& filter : filters_) {
-    auto status = filter->VerifyContext(context);
-
-    if (!status.ok()) {
-      return status;
-    }
-  }
-
-  if (KeepEvent(context, *packet)) {
-    return base::OkStatus();
-  }
-
-  packet->clear();
+base::Status TracePacketFilter::VerifyContext(const Context&) const {
   return base::OkStatus();
 }
 
-// Logical AND of all filters.
-bool ScrubTracePacket::KeepEvent(const Context& context,
-                                 const std::string& bytes) const {
+base::Status ScrubTracePacket::Transform(const Context& context,
+                                         std::string* packet) const {
+  if (packet == nullptr || packet->empty()) {
+    return base::ErrStatus("ScrubTracePacket: null or empty packet.");
+  }
+
   for (const auto& filter : filters_) {
-    if (!filter->KeepPacket(context, bytes)) {
+    RETURN_IF_ERROR(filter->VerifyContext(context));
+  }
+
+  protozero::HeapBuffered<protos::pbzero::TracePacket> new_packet;
+
+  protozero::ProtoDecoder decoder(*packet);
+
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (KeepEvent(context, field)) {
+      proto_util::AppendField(field, new_packet.get());
+    }
+  }
+
+  packet->assign(new_packet.SerializeAsString());
+  return base::OkStatus();
+}
+
+// Logical AND all filters.
+bool ScrubTracePacket::KeepEvent(const Context& context,
+                                 const protozero::Field& field) const {
+  for (const auto& filter : filters_) {
+    if (!filter->KeepField(context, field)) {
       return false;
     }
   }
diff --git a/src/trace_redaction/scrub_trace_packet.h b/src/trace_redaction/scrub_trace_packet.h
index 38dd2f1..22e506b 100644
--- a/src/trace_redaction/scrub_trace_packet.h
+++ b/src/trace_redaction/scrub_trace_packet.h
@@ -26,10 +26,12 @@
   virtual ~TracePacketFilter();
 
   // Checks if the context contains all neccessary parameters.
-  virtual base::Status VerifyContext(const Context& context) const = 0;
+  virtual base::Status VerifyContext(const Context& context) const;
 
-  virtual bool KeepPacket(const Context& context,
-                          const std::string& bytes) const = 0;
+  // Checks if the field should be pass onto the new packet. Checks are a
+  // logical AND, so all filters must return true.
+  virtual bool KeepField(const Context& context,
+                         const protozero::Field& field) const = 0;
 };
 
 class ScrubTracePacket : public TransformPrimitive {
@@ -43,7 +45,7 @@
   }
 
  private:
-  bool KeepEvent(const Context& context, const std::string& bytes) const;
+  bool KeepEvent(const Context& context, const protozero::Field& field) const;
 
   std::vector<std::unique_ptr<TracePacketFilter>> filters_;
 };
diff --git a/src/trace_redaction/suspend_resume.cc b/src/trace_redaction/suspend_resume.cc
new file mode 100644
index 0000000..243446f
--- /dev/null
+++ b/src/trace_redaction/suspend_resume.cc
@@ -0,0 +1,65 @@
+/*
+ * 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/suspend_resume.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/power.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+base::Status AllowSuspendResume::Build(Context* context) const {
+  context->ftrace_packet_allow_list.insert(
+      protos::pbzero::FtraceEvent::kSuspendResumeFieldNumber);
+
+  // Values are taken from "suspend_period.textproto".
+  context->suspend_result_allow_list.insert("syscore_suspend");
+  context->suspend_result_allow_list.insert("syscore_resume");
+  context->suspend_result_allow_list.insert("timekeeping_freeze");
+
+  return base::OkStatus();
+}
+
+base::Status FilterSuspendResume::VerifyContext(const Context&) const {
+  // FilterSuspendResume could check if kSuspendResumeFieldNumber is present in
+  // ftrace_packet_allow_list and there are values in the
+  // suspend_result_allow_list, but would make it hard to enable/disable
+  // suspend-resume redaction.
+  return base::OkStatus();
+}
+
+// The ftrace event is passed in.
+bool FilterSuspendResume::KeepEvent(const Context& context,
+                                    protozero::ConstBytes bytes) const {
+  protozero::ProtoDecoder event_decoder(bytes);
+
+  auto suspend_resume = event_decoder.FindField(
+      protos::pbzero::FtraceEvent::kSuspendResumeFieldNumber);
+
+  // It's not a suspend-resume event, defer the decision to another filter.
+  if (!suspend_resume.valid()) {
+    return true;
+  }
+
+  protozero::ProtoDecoder suspend_resume_decoder(suspend_resume.as_bytes());
+
+  auto action = suspend_resume_decoder.FindField(
+      protos::pbzero::SuspendResumeFtraceEvent::kActionFieldNumber);
+
+  return !action.valid() ||
+         context.suspend_result_allow_list.count(action.as_std_string());
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/suspend_resume.h b/src/trace_redaction/suspend_resume.h
new file mode 100644
index 0000000..09f7bd8
--- /dev/null
+++ b/src/trace_redaction/suspend_resume.h
@@ -0,0 +1,43 @@
+/*
+ * 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_SUSPEND_RESUME_H_
+#define SRC_TRACE_REDACTION_SUSPEND_RESUME_H_
+
+#include "src/trace_redaction/scrub_ftrace_events.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Updates allowlists to include suspend-resume events and which events to allow
+// through.
+class AllowSuspendResume : public BuildPrimitive {
+ public:
+  base::Status Build(Context* context) const override;
+};
+
+// Filters ftrace events based on the suspend-resume event.
+class FilterSuspendResume : public FtraceEventFilter {
+ public:
+  base::Status VerifyContext(const Context& context) const override;
+
+  bool KeepEvent(const Context& context,
+                 protozero::ConstBytes bytes) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_SUSPEND_RESUME_H_
diff --git a/src/trace_redaction/suspend_resume_unittest.cc b/src/trace_redaction/suspend_resume_unittest.cc
new file mode 100644
index 0000000..7cf498a
--- /dev/null
+++ b/src/trace_redaction/suspend_resume_unittest.cc
@@ -0,0 +1,149 @@
+/*
+ * 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/suspend_resume.h"
+#include "src/base/test/status_matchers.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/ftrace/ftrace.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.gen.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/power.gen.h"
+
+namespace perfetto::trace_redaction {
+
+TEST(AllowSuspendResumeTest, UpdatesTracePacketAllowlist) {
+  Context context;
+
+  // Start with a non-empty allow-list item.
+  context.ftrace_packet_allow_list.insert(
+      protos::pbzero::FtraceEvent::kPrintFieldNumber);
+
+  ASSERT_EQ(context.ftrace_packet_allow_list.size(), 1u);
+
+  AllowSuspendResume allow;
+  auto status = allow.Build(&context);
+  ASSERT_OK(status) << status.message();
+
+  // Print should still be present. The allowlist should have been updated, not
+  // replaced.
+  ASSERT_EQ(context.ftrace_packet_allow_list.count(
+                protos::pbzero::FtraceEvent::kPrintFieldNumber),
+            1u);
+
+  ASSERT_EQ(context.ftrace_packet_allow_list.count(
+                protos::pbzero::FtraceEvent::kSuspendResumeFieldNumber),
+            1u);
+}
+
+TEST(AllowSuspendResumeTest, UpdatesSuspendResumeAllowlist) {
+  Context context;
+
+  ASSERT_TRUE(context.suspend_result_allow_list.empty());
+
+  AllowSuspendResume allow;
+  auto status = allow.Build(&context);
+  ASSERT_OK(status) << status.message();
+
+  ASSERT_FALSE(context.suspend_result_allow_list.empty());
+}
+
+class SuspendResumeTest : public testing::Test {
+ protected:
+  void SetUp() {
+    AllowSuspendResume allow;
+    ASSERT_OK(allow.Build(&context_));
+  }
+
+  protos::gen::FtraceEvent CreateSuspendResumeEvent(
+      const std::string* action) const {
+    protos::gen::FtraceEvent event;
+    event.set_timestamp(1234);
+    event.set_pid(0);
+
+    auto* suspend_resume = event.mutable_suspend_resume();
+
+    if (action) {
+      suspend_resume->set_action(*action);
+    }
+
+    return event;
+  }
+
+  protos::gen::FtraceEvent CreateOtherEvent() const {
+    protos::gen::FtraceEvent event;
+    event.set_timestamp(1234);
+    event.set_pid(0);
+
+    auto* print = event.mutable_print();
+    print->set_buf("This is a message");
+
+    return event;
+  }
+
+  const Context& context() const { return context_; }
+
+ private:
+  Context context_;
+};
+
+// The suspend-resume filter is not responsible for non-suspend-resume events.
+// It should assume that another filter will handle it and it should just allow
+// those events through
+TEST_F(SuspendResumeTest, AcceptsOtherEvents) {
+  auto event = CreateOtherEvent();
+  auto event_array = event.SerializeAsArray();
+  protozero::ConstBytes event_bytes{event_array.data(), event_array.size()};
+
+  FilterSuspendResume filter;
+  ASSERT_TRUE(filter.KeepEvent(context(), event_bytes));
+}
+
+TEST_F(SuspendResumeTest, AcceptsEventsWithNoName) {
+  auto event = CreateSuspendResumeEvent(nullptr);
+  auto event_array = event.SerializeAsArray();
+  protozero::ConstBytes event_bytes{event_array.data(), event_array.size()};
+
+  Context context;
+
+  FilterSuspendResume filter;
+  ASSERT_TRUE(filter.KeepEvent(context, event_bytes));
+}
+
+TEST_F(SuspendResumeTest, AcceptsEventsWithValidName) {
+  // This value is from "src/trace_redaction/suspend_resume.cc".
+  std::string name = "syscore_suspend";
+
+  auto event = CreateSuspendResumeEvent(&name);
+  auto event_array = event.SerializeAsArray();
+  protozero::ConstBytes event_bytes{event_array.data(), event_array.size()};
+
+  FilterSuspendResume filter;
+  ASSERT_TRUE(filter.KeepEvent(context(), event_bytes));
+}
+
+TEST_F(SuspendResumeTest, RejectsEventsWithInvalidName) {
+  std::string name = "hello world";
+
+  auto event = CreateSuspendResumeEvent(&name);
+  auto event_array = event.SerializeAsArray();
+  protozero::ConstBytes event_bytes{event_array.data(), event_array.size()};
+
+  FilterSuspendResume filter;
+  ASSERT_FALSE(filter.KeepEvent(context(), event_bytes));
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_framework.cc b/src/trace_redaction/trace_redaction_framework.cc
index e3b729a..477f9ce 100644
--- a/src/trace_redaction/trace_redaction_framework.cc
+++ b/src/trace_redaction/trace_redaction_framework.cc
@@ -20,6 +20,14 @@
 
 CollectPrimitive::~CollectPrimitive() = default;
 
+base::Status CollectPrimitive::Begin(Context*) const {
+  return base::OkStatus();
+}
+
+base::Status CollectPrimitive::End(Context*) const {
+  return base::OkStatus();
+}
+
 BuildPrimitive::~BuildPrimitive() = default;
 
 TransformPrimitive::~TransformPrimitive() = default;
diff --git a/src/trace_redaction/trace_redaction_framework.h b/src/trace_redaction/trace_redaction_framework.h
index 96f7bf1..cc135a9 100644
--- a/src/trace_redaction/trace_redaction_framework.h
+++ b/src/trace_redaction/trace_redaction_framework.h
@@ -21,13 +21,16 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <unordered_set>
+#include <vector>
 
 #include "perfetto/base/flat_set.h"
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/status_or.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/frame_cookie.h"
 #include "src/trace_redaction/process_thread_timeline.h"
 
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
 namespace perfetto::trace_redaction {
 
 // Multiple packages can share the same name. This is common when a device has
@@ -158,6 +161,17 @@
   //      ftrace event
   base::FlatSet<uint32_t> ftrace_packet_allow_list;
 
+  //  message SuspendResumeFtraceEvent {
+  //    optional string action = 1 [(datapol.semantic_type) = ST_NOT_REQUIRED];
+  //    optional int32 val = 2;
+  //    optional uint32 start = 3 [(datapol.semantic_type) = ST_NOT_REQUIRED];
+  //  }
+  //
+  // The "action" in SuspendResumeFtraceEvent is a free-form string. There are
+  // some know and expected values. Those values are stored here and all events
+  // who's action value is not found here, the ftrace event will be dropped.
+  base::FlatSet<std::string> suspend_result_allow_list;
+
   // The timeline is a query-focused data structure that connects a pid to a
   // uid at specific point in time.
   //
@@ -175,27 +189,59 @@
   // After Sort(), Flatten() and Reduce() can be called (optional) to improve
   // the practical look-up times (compared to theoretical look-up times).
   std::unique_ptr<ProcessThreadTimeline> timeline;
+
+  // All frame events:
+  //
+  //  - ActualDisplayFrame
+  //  - ActualSurfaceFrame
+  //  - ExpectedDisplayFrame
+  //  - ExpectedSurfaceFrame
+  //
+  // Connect a time, a pid, and a cookie value. Cookies are unqiue within a
+  // trace, so if a cookie was connected to the target package, it can always be
+  // used.
+  //
+  // End events (i.e. FrameEnd) only have a time and cookie value. The cookie
+  // value connects it to its start time.
+  //
+  // In the collect phase, all start events are collected and converted to a
+  // simpler structure.
+  //
+  // In the build phase, the cookies are filtered to only include the ones that
+  // belong to the target package. This is down in the build phase, and not the
+  // collect phase, because the timeline is needed to determine if the cookie
+  // belongs to the target package.
+  std::vector<FrameCookie> global_frame_cookies;
+
+  // The collect of cookies that belong to the target package. Because cookie
+  // values are unique within the scope of the trace, pid and time are no longer
+  // needed and a set can be used for faster queries.
+  std::unordered_set<int64_t> package_frame_cookies;
 };
 
-// Responsible for extracting low-level data from the trace and storing it in
-// the context.
+// Extracts low-level data from the trace and writes it into the context. The
+// life cycle of a collect primitive is:
+//
+//  primitive.Begin(&context);
+//
+//  for (auto& packet : packets) {
+//    primitive.Collect(packet, &context);
+//  }
+//
+//  primitive.End(&context);
 class CollectPrimitive {
  public:
-  // When a collect primitive has collected all necessary information, it can
-  // stop processing packets by returning kRetire. If the primitives wants to
-  // continue processing packets, it will return kNextPacket.
-  //
-  // If a collector encounters an unrecoverable error, base::ErrStatus() is
-  // returned.
-  enum class ContinueCollection : bool { kRetire = false, kNextPacket = true };
-
   virtual ~CollectPrimitive();
 
-  // Processes a packet and writes low-level data to the context. Returns
-  // kContinue if the primitive wants more data (i.e. next packet).
-  virtual base::StatusOr<ContinueCollection> Collect(
-      const protos::pbzero::TracePacket::Decoder& packet,
-      Context* context) const = 0;
+  // Called once before the first call to Collect(...).
+  virtual base::Status Begin(Context*) const;
+
+  // Reads a trace packet and updates the context.
+  virtual base::Status Collect(const protos::pbzero::TracePacket::Decoder&,
+                               Context*) const = 0;
+
+  // Called once after the last call to Collect(...).
+  virtual base::Status End(Context*) const;
 };
 
 // Responsible for converting low-level data from the context and storing it in
diff --git a/src/trace_redaction/trace_redactor.cc b/src/trace_redaction/trace_redactor.cc
index 49fcf8f..60e8188 100644
--- a/src/trace_redaction/trace_redactor.cc
+++ b/src/trace_redaction/trace_redactor.cc
@@ -19,7 +19,6 @@
 #include <cstddef>
 #include <string>
 #include <string_view>
-#include <vector>
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/file_utils.h"
@@ -28,6 +27,7 @@
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/util/status_macros.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
@@ -48,39 +48,27 @@
   base::ScopedMmap mapped =
       base::ReadMmapWholeFile(source_filename_str.c_str());
   if (!mapped.IsValid()) {
-    return base::ErrStatus("Failed to map pages for trace (%s)",
+    return base::ErrStatus("TraceRedactor: failed to map pages for trace (%s)",
                            source_filename_str.c_str());
   }
 
   trace_processor::TraceBlobView whole_view(
       trace_processor::TraceBlob::FromMmap(std::move(mapped)));
 
-  if (auto status = Collect(context, whole_view); !status.ok()) {
-    return status;
+  RETURN_IF_ERROR(Collect(context, whole_view));
+
+  for (const auto& builder : builders_) {
+    RETURN_IF_ERROR(builder->Build(context));
   }
 
-  if (auto status = Build(context); !status.ok()) {
-    return status;
-  }
-
-  if (auto status = Transform(*context, whole_view, std::string(dest_filename));
-      !status.ok()) {
-    return status;
-  }
-
-  return base::OkStatus();
+  return Transform(*context, whole_view, std::string(dest_filename));
 }
 
 base::Status TraceRedactor::Collect(
     Context* context,
     const trace_processor::TraceBlobView& view) const {
-  // Mask, marking which collectors should be ran. When a collector no longer
-  // needs to run, the value will be null.
-  std::vector<const CollectPrimitive*> collectors;
-  collectors.reserve(collectors_.size());
-
   for (const auto& collector : collectors_) {
-    collectors.push_back(collector.get());
+    RETURN_IF_ERROR(collector->Begin(context));
   }
 
   const Trace::Decoder trace_decoder(view.data(), view.length());
@@ -88,38 +76,13 @@
   for (auto packet_it = trace_decoder.packet(); packet_it; ++packet_it) {
     const TracePacket::Decoder packet(packet_it->as_bytes());
 
-    for (auto cit = collectors.begin(); cit != collectors.end();) {
-      auto status = (*cit)->Collect(packet, context);
-
-      if (!status.ok()) {
-        return status.status();
-      }
-
-      // If this collector has returned `kStop`, it means that it (and it alone)
-      // no longer needs to run. The driver (TraceRedactor) should not invoke it
-      // on any future packets.
-      if (status.value() == CollectPrimitive::ContinueCollection::kRetire) {
-        cit = collectors.erase(cit);
-      } else {
-        ++cit;
-      }
-    }
-
-    // If all the collectors have found what they were looking for, then there
-    // is no reason to continue through the trace.
-    if (collectors.empty()) {
-      break;
+    for (auto& collector : collectors_) {
+      RETURN_IF_ERROR(collector->Collect(packet, context));
     }
   }
 
-  return base::OkStatus();
-}
-
-base::Status TraceRedactor::Build(Context* context) const {
-  for (const auto& builder : builders_) {
-    if (auto status = builder->Build(context); !status.ok()) {
-      return status;
-    }
+  for (const auto& collector : collectors_) {
+    RETURN_IF_ERROR(collector->End(context));
   }
 
   return base::OkStatus();
@@ -149,10 +112,7 @@
         break;
       }
 
-      if (auto status = transformer->Transform(context, &packet);
-          !status.ok()) {
-        return status;
-      }
+      RETURN_IF_ERROR(transformer->Transform(context, &packet));
     }
 
     // The packet has been removed from the trace. Don't write an empty packet
@@ -168,7 +128,8 @@
     if (const auto exported_data =
             base::WriteAll(dest_fd.get(), packet.data(), packet.size());
         exported_data <= 0) {
-      return base::ErrStatus("Failed to write redacted trace to disk");
+      return base::ErrStatus(
+          "TraceRedactor: failed to write redacted trace to disk");
     }
   }
 
diff --git a/src/trace_redaction/trace_redactor_integrationtest.cc b/src/trace_redaction/trace_redactor_integrationtest.cc
deleted file mode 100644
index a62359f..0000000
--- a/src/trace_redaction/trace_redactor_integrationtest.cc
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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 "src/base/test/status_matchers.h"
-#include "src/base/test/tmp_dir_tree.h"
-#include "src/base/test/utils.h"
-#include "src/trace_redaction/find_package_uid.h"
-#include "src/trace_redaction/prune_package_list.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/android/packages_list.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
-#include "protos/perfetto/trace/trace.pbzero.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-
-namespace perfetto::trace_redaction {
-
-// TODO(vaage): Add tests for the untested and/or included primitives:
-//
-//              1. Scrub process tree
-
-namespace {
-using FtraceEvent = protos::pbzero::FtraceEvent;
-
-constexpr std::string_view kTracePath =
-    "test/data/trace-redaction-general.pftrace";
-
-// Set the package name to "just some package name". If a specific package name
-// is needed, the test it should overwrite this value.
-constexpr std::string_view kPackageName =
-    "com.Unity.com.unity.multiplayer.samples.coop";
-constexpr uint64_t kPackageUid = 10252;
-
-class TraceRedactorIntegrationTest : public testing::Test {
- protected:
-  void SetUp() override {
-    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
-    context_.package_name = kPackageName;
-  }
-
-  const std::string& src_trace() const { return src_trace_; }
-
-  static base::StatusOr<std::string> ReadRawTrace(const std::string& path) {
-    std::string redacted_buffer;
-
-    if (base::ReadFile(path, &redacted_buffer)) {
-      return redacted_buffer;
-    }
-
-    return base::ErrStatus("Failed to read %s", path.c_str());
-  }
-
-  std::string src_trace_;
-  base::TmpDirTree tmp_dir_;
-
-  Context context_;
-  TraceRedactor redactor_;
-};
-
-class PackageListTraceRedactorIntegrationTest
-    : public TraceRedactorIntegrationTest {
- protected:
-  void SetUp() override {
-    TraceRedactorIntegrationTest::SetUp();
-
-    redactor_.emplace_collect<FindPackageUid>();
-    redactor_.emplace_transform<PrunePackageList>();
-  }
-
-  std::vector<protozero::ConstBytes> GetPackageInfos(
-      const protos::pbzero::Trace::Decoder& trace) const {
-    std::vector<protozero::ConstBytes> infos;
-
-    for (auto packet_it = trace.packet(); packet_it; ++packet_it) {
-      protos::pbzero::TracePacket::Decoder packet_decoder(*packet_it);
-      if (packet_decoder.has_packages_list()) {
-        protos::pbzero::PackagesList::Decoder list_it(
-            packet_decoder.packages_list());
-        for (auto info_it = list_it.packages(); info_it; ++info_it) {
-          protos::pbzero::PackagesList::PackageInfo::Decoder info(*info_it);
-          infos.push_back(*info_it);
-        }
-      }
-    }
-
-    return infos;
-  }
-};
-
-TEST_F(PackageListTraceRedactorIntegrationTest,
-       FindsPackageAndFiltersPackageList) {
-  auto result = redactor_.Redact(
-      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
-
-  ASSERT_OK(result) << result.message();
-
-  tmp_dir_.TrackFile("dst.pftrace");
-
-  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
-                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
-
-  protos::pbzero::Trace::Decoder redacted_trace(redacted_buffer);
-  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
-
-  ASSERT_TRUE(context_.package_uid.has_value());
-  ASSERT_EQ(NormalizeUid(context_.package_uid.value()),
-            NormalizeUid(kPackageUid));
-
-  // It is possible for two packages_list to appear in the trace. The
-  // find_package_uid will stop after the first one is found. Package uids are
-  // appear as n * 1,000,000 where n is some integer. It is also possible for
-  // two packages_list to contain copies of each other - for example
-  // "com.Unity.com.unity.multiplayer.samples.coop" appears in both
-  // packages_list.
-  ASSERT_EQ(infos.size(), 2u);
-
-  std::vector<protos::pbzero::PackagesList::PackageInfo::Decoder> decoders;
-  decoders.emplace_back(infos[0]);
-  decoders.emplace_back(infos[1]);
-
-  for (auto& decoder : decoders) {
-    ASSERT_TRUE(decoder.has_name());
-    ASSERT_EQ(decoder.name().ToStdString(),
-              "com.Unity.com.unity.multiplayer.samples.coop");
-
-    ASSERT_TRUE(decoder.has_uid());
-    ASSERT_EQ(NormalizeUid(decoder.uid()), NormalizeUid(kPackageUid));
-  }
-}
-
-// It is possible for multiple packages to share a uid. The names will appears
-// across multiple package lists. The only time the package name appears is in
-// the package list, so there is no way to differentiate these packages (only
-// the uid is used later), so each entry should remain.
-TEST_F(PackageListTraceRedactorIntegrationTest, RetainsAllInstancesOfUid) {
-  context_.package_name = "com.google.android.networkstack.tethering";
-
-  auto result = redactor_.Redact(
-      src_trace(), tmp_dir_.AbsolutePath("dst.pftrace"), &context_);
-
-  ASSERT_OK(result) << result.message();
-
-  tmp_dir_.TrackFile("dst.pftrace");
-
-  ASSERT_OK_AND_ASSIGN(auto redacted_buffer,
-                       ReadRawTrace(tmp_dir_.AbsolutePath("dst.pftrace")));
-
-  protos::pbzero::Trace::Decoder redacted_trace(redacted_buffer);
-  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
-
-  ASSERT_EQ(infos.size(), 8u);
-
-  std::array<std::string, 8> package_names;
-
-  for (size_t i = 0; i < infos.size(); ++i) {
-    protos::pbzero::PackagesList::PackageInfo::Decoder info(infos[i]);
-    ASSERT_TRUE(info.has_name());
-    package_names[i] = info.name().ToStdString();
-  }
-
-  std::sort(package_names.begin(), package_names.end());
-  ASSERT_EQ(package_names[0], "com.google.android.cellbroadcastservice");
-  ASSERT_EQ(package_names[1], "com.google.android.cellbroadcastservice");
-  ASSERT_EQ(package_names[2], "com.google.android.networkstack");
-  ASSERT_EQ(package_names[3], "com.google.android.networkstack");
-  ASSERT_EQ(package_names[4],
-            "com.google.android.networkstack.permissionconfig");
-  ASSERT_EQ(package_names[5],
-            "com.google.android.networkstack.permissionconfig");
-  ASSERT_EQ(package_names[6], "com.google.android.networkstack.tethering");
-  ASSERT_EQ(package_names[7], "com.google.android.networkstack.tethering");
-}
-
-}  // namespace
-}  // namespace perfetto::trace_redaction
diff --git a/src/traced/probes/ftrace/OWNERS b/src/traced/probes/ftrace/OWNERS
index 66ecb6c..52d5e50 100644
--- a/src/traced/probes/ftrace/OWNERS
+++ b/src/traced/probes/ftrace/OWNERS
@@ -1,5 +1,4 @@
 # People knowledgeable with traced_probes <> ftrace integration.
-hjd@google.com
 primiano@google.com
 rsavitski@google.com
 skyostil@google.com
diff --git a/src/traced/probes/ftrace/atrace_wrapper.cc b/src/traced/probes/ftrace/atrace_wrapper.cc
index 1a64544..38f0200 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.cc
+++ b/src/traced/probes/ftrace/atrace_wrapper.cc
@@ -187,18 +187,18 @@
 #endif
 }
 
-bool AtraceWrapperImpl::IsOldAtrace() {
+bool AtraceWrapperImpl::SupportsUserspaceOnly() {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) && \
     !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
   // Sideloaded case. We could be sideloaded on a modern device or an older one.
   std::string str_value = base::GetAndroidProp("ro.build.version.sdk");
   if (str_value.empty())
-    return false;
+    return true;
   auto opt_value = base::CStringToUInt32(str_value.c_str());
-  return opt_value.has_value() && *opt_value < 28;  // 28 == Android P.
+  return !opt_value.has_value() || *opt_value >= 28;  // 28 == Android P.
 #else
   // In in-tree builds we know that atrace is current, no runtime checks needed.
-  return false;
+  return true;
 #endif
 }
 
diff --git a/src/traced/probes/ftrace/atrace_wrapper.h b/src/traced/probes/ftrace/atrace_wrapper.h
index 5c6a148..18c3d2d 100644
--- a/src/traced/probes/ftrace/atrace_wrapper.h
+++ b/src/traced/probes/ftrace/atrace_wrapper.h
@@ -30,7 +30,7 @@
   // - Just use atrace --async_start/stop, which will cause atrace to also
   //   poke at ftrace.
   // - Suppress the checks for "somebody else enabled ftrace unexpectedly".
-  virtual bool IsOldAtrace() = 0;
+  virtual bool SupportsUserspaceOnly() = 0;
   virtual bool RunAtrace(const std::vector<std::string>& args,
                          std::string* atrace_errors) = 0;
 };
@@ -38,7 +38,7 @@
 class AtraceWrapperImpl : public AtraceWrapper {
  public:
   ~AtraceWrapperImpl() override;
-  bool IsOldAtrace() override;
+  bool SupportsUserspaceOnly() override;
   bool RunAtrace(const std::vector<std::string>& args,
                  std::string* atrace_errors) override;
 };
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index e11184b..09615fe 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -7597,16 +7597,22 @@
        "panel",
        {
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "type", 1, ProtoSchemaType::kUint32,
+            "pid", 1, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "pid", 2, ProtoSchemaType::kInt32,
+            "trace_name", 2, ProtoSchemaType::kString,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "name", 3, ProtoSchemaType::kString,
+            "trace_begin", 3, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "value", 4, ProtoSchemaType::kInt32,
+            "name", 4, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "type", 5, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "value", 6, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
@@ -7616,55 +7622,55 @@
        "perf_trace_counters",
        {
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "prev_comm", 1, ProtoSchemaType::kString,
+            "old_pid", 1, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "prev_pid", 2, ProtoSchemaType::kInt32,
+            "new_pid", 2, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cyc", 3, ProtoSchemaType::kUint32,
+            "cctr", 3, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "inst", 4, ProtoSchemaType::kUint32,
+            "ctr0", 4, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "stallbm", 5, ProtoSchemaType::kUint32,
+            "ctr1", 5, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "l3dm", 6, ProtoSchemaType::kUint32,
+            "ctr2", 6, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "old_pid", 7, ProtoSchemaType::kInt32,
+            "ctr3", 7, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "new_pid", 8, ProtoSchemaType::kInt32,
+            "lctr0", 8, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cctr", 9, ProtoSchemaType::kUint32,
+            "lctr1", 9, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr0", 10, ProtoSchemaType::kUint32,
+            "ctr4", 10, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr1", 11, ProtoSchemaType::kUint32,
+            "ctr5", 11, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr2", 12, ProtoSchemaType::kUint32,
+            "prev_comm", 12, ProtoSchemaType::kString,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr3", 13, ProtoSchemaType::kUint32,
+            "prev_pid", 13, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "lctr0", 14, ProtoSchemaType::kUint32,
+            "cyc", 14, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "lctr1", 15, ProtoSchemaType::kUint32,
+            "inst", 15, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr4", 16, ProtoSchemaType::kUint32,
+            "stallbm", 16, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr5", 17, ProtoSchemaType::kUint32,
+            "l3dm", 17, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index e4d8a19..f8a0eff 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -656,7 +656,7 @@
           "atrace_apps options as they affect global state");
       return false;
     }
-    if (atrace_wrapper_->IsOldAtrace() && !ds_configs_.empty()) {
+    if (!atrace_wrapper_->SupportsUserspaceOnly() && !ds_configs_.empty()) {
       PERFETTO_ELOG(
           "Concurrent atrace sessions are not supported before Android P, "
           "bailing out.");
@@ -1050,7 +1050,7 @@
   std::vector<std::string> args;
   args.push_back("atrace");  // argv0 for exec()
   args.push_back("--async_start");
-  if (!atrace_wrapper_->IsOldAtrace())
+  if (atrace_wrapper_->SupportsUserspaceOnly())
     args.push_back("--only_userspace");
 
   for (const auto& category : categories)
@@ -1078,7 +1078,7 @@
   PERFETTO_DLOG("Stop atrace...");
 
   std::vector<std::string> args{"atrace", "--async_stop"};
-  if (!atrace_wrapper_->IsOldAtrace())
+  if (atrace_wrapper_->SupportsUserspaceOnly())
     args.push_back("--only_userspace");
   if (atrace_wrapper_->RunAtrace(args, /*atrace_errors=*/nullptr)) {
     current_state_.atrace_categories.clear();
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
index 513d851..9e9ca5e 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer_unittest.cc
@@ -96,7 +96,7 @@
 class MockAtraceWrapper : public AtraceWrapper {
  public:
   MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
-  MOCK_METHOD(bool, IsOldAtrace, ());
+  MOCK_METHOD(bool, SupportsUserspaceOnly, ());
 };
 
 class MockProtoTranslationTable : public ProtoTranslationTable {
@@ -173,7 +173,7 @@
  protected:
   FtraceConfigMuxerTest() {
     ON_CALL(atrace_wrapper_, RunAtrace).WillByDefault(Return(true));
-    ON_CALL(atrace_wrapper_, IsOldAtrace).WillByDefault(Return(false));
+    ON_CALL(atrace_wrapper_, SupportsUserspaceOnly).WillByDefault(Return(true));
   }
 
   std::unique_ptr<MockProtoTranslationTable> GetMockTable() {
diff --git a/src/traced/probes/ftrace/ftrace_controller_unittest.cc b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
index 1a3538e..bfcb516 100644
--- a/src/traced/probes/ftrace/ftrace_controller_unittest.cc
+++ b/src/traced/probes/ftrace/ftrace_controller_unittest.cc
@@ -205,7 +205,7 @@
 class MockAtraceWrapper : public AtraceWrapper {
  public:
   MOCK_METHOD(bool, RunAtrace, (const std::vector<std::string>&, std::string*));
-  MOCK_METHOD(bool, IsOldAtrace, ());
+  MOCK_METHOD(bool, SupportsUserspaceOnly, ());
 };
 
 }  // namespace
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index b71f0b8..cd1207c 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -4928,25 +4928,31 @@
 
   auto trace = StopSessionAndReturnParsedTrace(tracing_session);
 
-  // Check that clock snapshots are monotonic and don't contain timestamps from
-  // trace events with explicit timestamps.
-  struct LastTs {
-    uint64_t ts = 0;
+  // Check that clock snapshots are monotonic (per sequence) and don't contain
+  // timestamps from trace events with explicit timestamps.
+  struct ClockPerSequence {
     uint64_t seq_id = 0;
+    uint64_t clock_id = 0;
+    bool operator<(const struct ClockPerSequence& other) const {
+      return std::tie(seq_id, clock_id) <
+             std::tie(other.seq_id, other.clock_id);
+    }
   };
-  std::unordered_map<uint64_t, LastTs> last_clock_ts;
+  std::map<ClockPerSequence, uint64_t> last_clock_ts;
   for (const auto& packet : trace.packet()) {
     if (packet.has_clock_snapshot()) {
       for (auto& clock : packet.clock_snapshot().clocks()) {
         if (!clock.is_incremental()) {
           uint64_t ts = clock.timestamp();
-          uint64_t id = clock.clock_id();
-          EXPECT_LE(last_clock_ts[id].ts, ts)
-              << "This sequence:" << packet.trusted_packet_sequence_id()
-              << " prev sequence:" << last_clock_ts[id].seq_id
-              << " clock_id:" << id;
-          last_clock_ts[id].ts = ts;
-          last_clock_ts[id].seq_id = packet.trusted_packet_sequence_id();
+          ClockPerSequence c;
+          c.seq_id = packet.trusted_packet_sequence_id();
+          c.clock_id = clock.clock_id();
+
+          uint64_t& last = last_clock_ts[c];
+
+          EXPECT_LE(last, ts)
+              << "This sequence:" << c.seq_id << " clock_id:" << c.clock_id;
+          last = ts;
         }
       }
 
diff --git a/test/cts/OWNERS b/test/cts/OWNERS
index 10fd9fe..0a7edbb 100644
--- a/test/cts/OWNERS
+++ b/test/cts/OWNERS
@@ -1,6 +1,5 @@
 # Bug component: 323270
 ddiproietto@google.com
-hjd@google.com
 lalitm@google.com
 primiano@google.com
 rsavitski@google.com
diff --git a/test/cts/heapprofd_test_cts.cc b/test/cts/heapprofd_test_cts.cc
index c4eb97f..99d8c50 100644
--- a/test/cts/heapprofd_test_cts.cc
+++ b/test/cts/heapprofd_test_cts.cc
@@ -88,8 +88,8 @@
     bool multiuser_support = sdk && *sdk >= 34;
     cmd_ = "content read";
     if (multiuser_support) {
-      // This command is available only starting from android U.
-      cmd_ += " --user `cmd user get-main-user`";
+      // This is required only starting from android U.
+      cmd_ += " --user `am get-current-user`";
     }
     cmd_ += std::string(" --uri content://") + app + std::string("/") + path;
     cmd_ += " >" + tempfile_;
diff --git a/test/data/api24_startup_cold.perfetto-trace.sha256 b/test/data/api24_startup_cold.perfetto-trace.sha256
new file mode 100644
index 0000000..dbc4e62
--- /dev/null
+++ b/test/data/api24_startup_cold.perfetto-trace.sha256
@@ -0,0 +1 @@
+ac006a3ec74fac70feb58c2cfad840c552af0ebd25f03bdfbf14d5f77148764b
\ No newline at end of file
diff --git a/test/data/api24_startup_hot.perfetto-trace.sha256 b/test/data/api24_startup_hot.perfetto-trace.sha256
new file mode 100644
index 0000000..6fa48c6
--- /dev/null
+++ b/test/data/api24_startup_hot.perfetto-trace.sha256
@@ -0,0 +1 @@
+0ec527c6392adf16e8580ba0fa3eaccf69eccba36c863377eca5c5618f00f3ea
\ No newline at end of file
diff --git a/test/data/api24_startup_warm.perfetto-trace.sha256 b/test/data/api24_startup_warm.perfetto-trace.sha256
new file mode 100644
index 0000000..04eca80
--- /dev/null
+++ b/test/data/api24_startup_warm.perfetto-trace.sha256
@@ -0,0 +1 @@
+59d1e84ddf6a492c10d7b6ecebf7b80813e6a0a2b3bef7d854d5ef3cf7210137
\ No newline at end of file
diff --git a/test/data/api31_startup_cold.perfetto-trace.sha256 b/test/data/api31_startup_cold.perfetto-trace.sha256
new file mode 100644
index 0000000..98598a7
--- /dev/null
+++ b/test/data/api31_startup_cold.perfetto-trace.sha256
@@ -0,0 +1 @@
+aadf4a141dc0249abd4d7080a7d94bd614949eb0e7398fa6f3c75b503c70dade
\ No newline at end of file
diff --git a/test/data/api31_startup_hot.perfetto-trace.sha256 b/test/data/api31_startup_hot.perfetto-trace.sha256
new file mode 100644
index 0000000..8cac597
--- /dev/null
+++ b/test/data/api31_startup_hot.perfetto-trace.sha256
@@ -0,0 +1 @@
+072802c0adf6968d0eb9c56fa13b33cad32f583398d019dc8a798981164333c5
\ No newline at end of file
diff --git a/test/data/api31_startup_warm.perfetto-trace.sha256 b/test/data/api31_startup_warm.perfetto-trace.sha256
new file mode 100644
index 0000000..e04f245
--- /dev/null
+++ b/test/data/api31_startup_warm.perfetto-trace.sha256
@@ -0,0 +1 @@
+4655c161656bf0eb66e8bc046ff899813e08bcf73f5e253ca88e8b3be231cb0e
\ No newline at end of file
diff --git a/test/data/chrome/scroll_offsets_trace_2.pftrace.sha256 b/test/data/chrome/scroll_offsets_trace_2.pftrace.sha256
new file mode 100644
index 0000000..27d8ace
--- /dev/null
+++ b/test/data/chrome/scroll_offsets_trace_2.pftrace.sha256
@@ -0,0 +1 @@
+2ddd9f78d91d51e39c72c520bb54fdc9dbf1333ae722e87633fc345159296289
\ No newline at end of file
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 a359061..6c9da12 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 @@
-4b4bd13cbae5710efda25c6e2495c7ec5614d348701b9fec1fd554d5b1c61064
\ No newline at end of file
+3b1182b4e24fa80004c5bcec56c2cc9b4814ed9da7266fe54fafde4191ab3b4f
\ 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 02b1315..4a77c81 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 @@
-687a6fc2a478b048bdcbbbfe6231cf3b33463068c8100659a5be0220f41d07fb
\ No newline at end of file
+07bcd60e3622fd39e0270962294696c7280c3cc45997837ae136ea1da0f8d094
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256 b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
index 905ad76..c8cc93d 100644
--- a/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
+++ b/test/data/ui-screenshots/ui-chrome_rendering_desktop_expand_browser_proc.png.sha256
@@ -1 +1 @@
-c1fa13737c59bf1eeaa997037ca99117625b4d7956496702734317555ff5a2bc
\ No newline at end of file
+193a54d8a03494937a21ecd2210b926a73e43f3d9617d622328e9a45cd32b017
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 1f711b8..cb82c55 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -92,6 +92,7 @@
 from diff_tests.parser.translated_args.tests import TranslatedArgs
 from diff_tests.parser.ufs.tests import Ufs
 from diff_tests.stdlib.android.frames_tests import Frames
+from diff_tests.stdlib.android.startups_tests import Startups
 from diff_tests.stdlib.android.tests import AndroidStdlib
 from diff_tests.stdlib.chrome.chrome_stdlib_testsuites import CHROME_STDLIB_TESTSUITES
 from diff_tests.stdlib.common.tests import StdlibCommon
@@ -282,6 +283,7 @@
                        'StdlibIntervals').fetch(),
       *IntervalsIntersect(index_path, 'stdlib/intervals',
                           'StdlibIntervalsIntersect').fetch(),
+      *Startups(index_path, 'stdlib/android', 'Startups').fetch(),
       *Timestamps(index_path, 'stdlib/timestamps', 'Timestamps').fetch(),
       *WattsonStdlib(index_path, 'stdlib/wattson', 'WattsonStdlib').fetch(),
   ] + chrome_stdlib_tests
diff --git a/test/trace_processor/diff_tests/metrics/android/android_boot.out b/test/trace_processor/diff_tests/metrics/android/android_boot.out
index 3bf13cc..50f4757 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_boot.out
+++ b/test/trace_processor/diff_tests/metrics/android/android_boot.out
@@ -1,59 +1,915 @@
 android_boot {
-    system_server_durations {
-        total_dur: 90219646678
-        uninterruptible_sleep_dur: 618417159
-    }
-    systemui_durations {
-        total_dur: 48481027953
-        uninterruptible_sleep_dur: 796263
-    }
-    launcher_durations {
-        total_dur: 23595248987
-        uninterruptible_sleep_dur: 257290255
-    }
-    gms_durations {
-        total_dur: 27804143410
-        uninterruptible_sleep_dur: 101685087
-    }
-    launcher_breakdown {
-        cold_start_dur: 403543498
-    }
-    full_trace_process_start_aggregation {
-        total_start_sum: 10678297679
-        num_of_processes: 29
-        average_start_time: 368217161.3448276
-    }
-    post_boot_process_start_aggregation {
-        total_start_sum: 6112984648
-        num_of_processes: 21
-        average_start_time: 291094507.04761904
-    }
-    full_trace_gc_aggregation {
-        total_gc_count: 4
-        num_of_processes_with_gc: 4
-        num_of_threads_with_gc: 4
-        avg_gc_duration: 260516077.75
-        avg_running_gc_duration: 3902628.5
-        full_gc_count: 4
-        collector_transition_gc_count: 0
-        young_gc_count: 0
-        native_alloc_gc_count: 0
-        explicit_gc_count: 0
-        alloc_gc_count: 0
-        mb_per_ms_of_gc: 0.8829305684617432
-    }
-    post_boot_gc_aggregation {
-        total_gc_count: 4
-        num_of_processes_with_gc: 4
-        num_of_threads_with_gc: 4
-        avg_gc_duration: 260516077.75
-        avg_running_gc_duration: 3902628.5
-        full_gc_count: 4
-        collector_transition_gc_count: 0
-        young_gc_count: 0
-        native_alloc_gc_count: 0
-        explicit_gc_count: 0
-        alloc_gc_count: 0
-        mb_per_ms_of_gc: 0.8829305684617432
-    }
-}
\ No newline at end of file
+system_server_durations {
+total_dur: 90219646678
+uninterruptible_sleep_dur: 618417159
+}
+systemui_durations {
+total_dur: 48481027953
+uninterruptible_sleep_dur: 796263
+}
+launcher_durations {
+total_dur: 23595248987
+uninterruptible_sleep_dur: 257290255
+}
+gms_durations {
+total_dur: 27804143410
+uninterruptible_sleep_dur: 101685087
+}
+launcher_breakdown {
+cold_start_dur: 403543498
+}
+full_trace_process_start_aggregation {
+total_start_sum: 10678297679
+num_of_processes: 29
+average_start_time: 368217161.3448276
+}
+post_boot_process_start_aggregation {
+total_start_sum: 6112984648
+num_of_processes: 21
+average_start_time: 291094507.04761904
+}
+full_trace_gc_aggregation {
+total_gc_count: 4
+num_of_processes_with_gc: 4
+num_of_threads_with_gc: 4
+avg_gc_duration: 260516077.75
+avg_running_gc_duration: 3902628.5
+full_gc_count: 4
+collector_transition_gc_count: 0
+young_gc_count: 0
+native_alloc_gc_count: 0
+explicit_gc_count: 0
+alloc_gc_count: 0
+mb_per_ms_of_gc: 0.8829305684617432
+}
+post_boot_gc_aggregation {
+total_gc_count: 4
+num_of_processes_with_gc: 4
+num_of_threads_with_gc: 4
+avg_gc_duration: 260516077.75
+avg_running_gc_duration: 3902628.5
+full_gc_count: 4
+collector_transition_gc_count: 0
+young_gc_count: 0
+native_alloc_gc_count: 0
+explicit_gc_count: 0
+alloc_gc_count: 0
+mb_per_ms_of_gc: 0.8829305684617432
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 16
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_global {
+dest_bucket: "foreground_app"
+count: 21
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 4
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_global {
+dest_bucket: "native"
+count: 4
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 4
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "/system/bin/apexd"
+dest_bucket: "native"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "<pre-initialized>"
+dest_bucket: "native"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "android.process.acore"
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.amazon.mp3"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.android.keychain"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.android.printspooler"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.android.providers.calendar"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.android.settings"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.apple.android.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.blinkslabs.blinkist.android"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.books"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging"
+dest_bucket: "native"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging:rcs"
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging:rcs"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.nexuslauncher"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wallpaper"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wellbeing"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 5
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wellbeing"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.work.clouddpc"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.youtube.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.calendar"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.carrier"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.cellbroadcastreceiver"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.deskclock"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.dialer"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 4
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.googlequicksearchbox:search"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.keep"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.permissioncontroller"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.providers.media.module"
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.android.youtube"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.ar.core"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.ar.core"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.google.euiccpixel"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.overdrive.mobile.android.libby"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.patreon.android"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "com.spotify.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "org.thoughtcrime.securesms"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_process {
+name: "org.videolan.vlc"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "bindService"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 5
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startReceiver"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 7
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "processBegin"
+dest_bucket: "foreground_app"
+count: 21
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startReceiver"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "unbindService"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "bindService"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+dest_bucket: "native"
+count: 4
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 3
+}
+post_boot_oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "finishReceiver"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "cached_app"
+total_dur: 10419196404
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "foreground_app"
+total_dur: 23641055480
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "native"
+total_dur: 101590616
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "perceptible_app"
+total_dur: 940071238
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "perceptible_low_app"
+total_dur: 199288116
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "persistent_service"
+total_dur: 1546461864
+}
+post_boot_oom_adj_bucket_duration_agg_global {
+bucket: "visible_app"
+total_dur: 2769744731
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/apexd"
+bucket: "native"
+total_dur: 25397654
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "<pre-initialized>"
+bucket: "native"
+total_dur: 50795308
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "android.process.acore"
+bucket: "foreground_app"
+total_dur: 823432282
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.amazon.mp3"
+bucket: "foreground_app"
+total_dur: 1322230378
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.android.keychain"
+bucket: "foreground_app"
+total_dur: 196867340
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.android.printspooler"
+bucket: "foreground_app"
+total_dur: 343904124
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.android.providers.calendar"
+bucket: "foreground_app"
+total_dur: 281386139
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.android.settings"
+bucket: "cached_app"
+total_dur: 967052521
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.apple.android.music"
+bucket: "foreground_app"
+total_dur: 1268519522
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.blinkslabs.blinkist.android"
+bucket: "foreground_app"
+total_dur: 1250421540
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.books"
+bucket: "foreground_app"
+total_dur: 1076963328
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "cached_app"
+total_dur: 1086183503
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "foreground_app"
+total_dur: 402609904
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "perceptible_low_app"
+total_dur: 7658935
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging"
+bucket: "native"
+total_dur: 25397654
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging:rcs"
+bucket: "foreground_app"
+total_dur: 109047811
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging:rcs"
+bucket: "perceptible_app"
+total_dur: 940071238
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.nexuslauncher"
+bucket: "foreground_app"
+total_dur: 1661132477
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wallpaper"
+bucket: "foreground_app"
+total_dur: 177919993
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wellbeing"
+bucket: "cached_app"
+total_dur: 816703328
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wellbeing"
+bucket: "foreground_app"
+total_dur: 884970246
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.work.clouddpc"
+bucket: "cached_app"
+total_dur: 1695452790
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.youtube.music"
+bucket: "foreground_app"
+total_dur: 1558635529
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "cached_app"
+total_dur: 621111806
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "foreground_app"
+total_dur: 303909018
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "perceptible_low_app"
+total_dur: 9431030
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.calendar"
+bucket: "foreground_app"
+total_dur: 1573637523
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.carrier"
+bucket: "cached_app"
+total_dur: 870239329
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.cellbroadcastreceiver"
+bucket: "cached_app"
+total_dur: 870311514
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.deskclock"
+bucket: "foreground_app"
+total_dur: 1674669994
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "cached_app"
+total_dur: 11603068
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "foreground_app"
+total_dur: 115664632
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "visible_app"
+total_dur: 1568444611
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.dialer"
+bucket: "cached_app"
+total_dur: 1701572215
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "cached_app"
+total_dur: 38437012
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "foreground_app"
+total_dur: 1259805736
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "visible_app"
+total_dur: 397030029
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+bucket: "visible_app"
+total_dur: 804270091
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.googlequicksearchbox:search"
+bucket: "foreground_app"
+total_dur: 1467442984
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.keep"
+bucket: "foreground_app"
+total_dur: 576153188
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.permissioncontroller"
+bucket: "cached_app"
+total_dur: 870388947
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.providers.media.module"
+bucket: "persistent_service"
+total_dur: 1546461864
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.youtube"
+bucket: "foreground_app"
+total_dur: 1450400991
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.ar.core"
+bucket: "foreground_app"
+total_dur: 733794515
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.ar.core"
+bucket: "perceptible_low_app"
+total_dur: 182198151
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.google.euiccpixel"
+bucket: "cached_app"
+total_dur: 870140371
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.overdrive.mobile.android.libby"
+bucket: "foreground_app"
+total_dur: 889039012
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.patreon.android"
+bucket: "foreground_app"
+total_dur: 608530141
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "com.spotify.music"
+bucket: "foreground_app"
+total_dur: 566682160
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "org.thoughtcrime.securesms"
+bucket: "foreground_app"
+total_dur: 701758738
+}
+post_boot_oom_adj_bucket_duration_agg_by_process {
+name: "org.videolan.vlc"
+bucket: "foreground_app"
+total_dur: 361526235
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 266887
+max_oom_adj_dur: 7292196
+avg_oom_adj_dur: 6121311.166666667
+oom_adj_event_count: 2
+oom_adj_reason: "activityChange"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 67180
+max_oom_adj_dur: 225871
+avg_oom_adj_dur: 160115.83333333334
+oom_adj_event_count: 5
+oom_adj_reason: "bindService"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 64657
+max_oom_adj_dur: 565674
+avg_oom_adj_dur: 232660.2142857143
+oom_adj_event_count: 11
+oom_adj_reason: "executingService"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 198120
+max_oom_adj_dur: 198120
+avg_oom_adj_dur: 198120.0
+oom_adj_event_count: 1
+oom_adj_reason: "finishReceiver"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 193766
+max_oom_adj_dur: 626302
+avg_oom_adj_dur: 383901.0
+oom_adj_event_count: 4
+oom_adj_reason: "getProvider"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 92366
+max_oom_adj_dur: 1461914
+avg_oom_adj_dur: 257078.14285714287
+oom_adj_event_count: 21
+oom_adj_reason: "processBegin"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 238525
+max_oom_adj_dur: 5160522
+avg_oom_adj_dur: 883870.1111111111
+oom_adj_event_count: 4
+oom_adj_reason: "startReceiver"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 68645
+max_oom_adj_dur: 2294270
+avg_oom_adj_dur: 747080.5
+oom_adj_event_count: 4
+oom_adj_reason: "startService"
+}
+post_boot_oom_adj_duration_agg {
+min_oom_adj_dur: 165242
+max_oom_adj_dur: 380290
+avg_oom_adj_dur: 272766.0
+oom_adj_event_count: 2
+oom_adj_reason: "unbindService"
+}
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/android_oom_adjuster.out b/test/trace_processor/diff_tests/metrics/android/android_oom_adjuster.out
new file mode 100644
index 0000000..e7135d0
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_oom_adjuster.out
@@ -0,0 +1,2745 @@
+android_oom_adjuster {
+oom_adjuster_transition_counts_global {
+dest_bucket: "cached_app"
+count: 7
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 27
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 5
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 3
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "cached_app"
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "previous_app"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "foreground_app"
+count: 27
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 4
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 4
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "logcat"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "native"
+count: 121
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 3
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "persistent_proc"
+count: 17
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "persistent_service"
+count: 2
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "previous_app"
+count: 5
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "service"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "system"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "unknown_native"
+count: 1
+}
+oom_adjuster_transition_counts_global {
+dest_bucket: "visible_app"
+count: 6
+}
+oom_adjuster_transition_counts_global {
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 4
+}
+oom_adjuster_transition_counts_by_process {
+name: ".ShannonImsService"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.android.adbd/bin/adbd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.android.hardware.cas/bin/hw/android.hardware.cas-service.example"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.android.os.statsd/bin/statsd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.google.android.hardware.biometrics.face/bin/hw/android.hardware.biometrics.face-service.22.pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.google.android.widevine/bin/hw/android.hardware.drm-service.widevine"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.google.pixel.camera.hal/bin/hw/android.hardware.camera.provider@2.7-service-google"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.google.pixel.camera.hal/bin/rlsservice"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/apex/com.google.pixel.wifi.ext/bin/hw/vendor.google.wifi_ext-service-vendor"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/apexd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/audioserver"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/cameraserver"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/credstore"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/drmserver"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/gatekeeperd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/gpuservice"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/hw/android.system.suspend-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/incidentd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/init"
+dest_bucket: "native"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/installd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/ip6tables-restore"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/iptables-restore"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/keystore2"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/lmkd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/logd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/mediaserver"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/netd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/prng_seeder"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/servicemanager"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/storaged"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/surfaceflinger"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/tombstoned"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/traced"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/traced_probes"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/ueventd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/update_engine"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/virtual_camera"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/vold"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/bin/wificond"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/system_ext/bin/hw/android.hidl.allocator@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/system_ext/bin/hwservicemanager"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system/vendor/bin/wfc-pkt-router"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system_ext/bin/gs_watchdogd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/system_ext/bin/hw/vendor.google.edgetpu_app_service@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/aocd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/bipchmgr"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/cbd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/chre"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/dmd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.audio.service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.authsecret-service.citadel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.biometrics.fingerprint-service.goodix"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.bluetooth-service.bcmbtlinux"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.boot-service.default-pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.cas@1.2-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.composer.hwc3-service.pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.confirmationui-service.trusty.vendor"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.contexthub-service.generic"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.drm-service.clearkey"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.dumpstate-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.edgetpu.logging@service-edgetpu-logging"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.gatekeeper-service.trusty"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.gnss@2.1-service-brcm"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.graphics.allocator-V2-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.gxp.logging@service-gxp-logging"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.health-service.gs201"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.identity@1.0-service.citadel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.input.processor-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.memtrack-service.pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.neuralnetworks@service-darwinn-aidl"
+dest_bucket: "native"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.nfc-service.st"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.oemlock-service.citadel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.power-service.pixel-libperfmgr"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.power.stats-service.pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-service-gto"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-service-gto-ese2"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-uicc-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.security.keymint-service.citadel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.security.keymint-service.trusty"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.usb-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.usb.gadget-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.vibrator-service.cs40l26"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/android.hardware.weaver-service.citadel"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/battery_mitigation"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/citadeld"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/google.hardware.media.c2@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/gpsd"
+dest_bucket: "unknown_native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/lhd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/rild_exynos"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/samsung.hardware.media.c2@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/scd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/vendor.dolby.media.c2@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/vendor.google.audiometricext@1.0-service-vendor"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/vendor.google.google_battery-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/vendor.google.wireless_charger-default"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/hw/wpa_supplicant"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/modem_svc_sit"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/pixelstats-vendor"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/ramdump"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/rfsd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/sced"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/securedpud.slider"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/sscoredump"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/storageproxyd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/trusty_metricsd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/twoshay"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/vcd"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "/vendor/bin/vndservicemanager"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "<pre-initialized>"
+dest_bucket: "native"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "android.process.acore"
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "android.process.acore"
+dest_bucket: "previous_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.amazon.mp3"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.hbmsvmanager"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.keychain"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.networkstack.process"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.nfc"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.phone"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.pixellogger"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.printspooler"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.providers.calendar"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.qns"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.se"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.settings"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.settings"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.android.systemui"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.apple.android.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.blinkslabs.blinkist.android"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.SSRestartDetector"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.books"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.cbrsnetworkmonitor"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.dreamliner"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging:rcs"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging:rcs"
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.messaging:rcs"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.nexuslauncher"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.nexuslauncher"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.scone"
+dest_bucket: "service"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wallpaper"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wellbeing"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wellbeing"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 8
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.wellbeing"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.work.clouddpc"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.work.clouddpc"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.work.clouddpc"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.work.clouddpc"
+src_bucket: "cached_app"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.apps.youtube.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.as.oss"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.bluetooth"
+dest_bucket: "persistent_service"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.calendar"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.carrier"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.carrier"
+dest_bucket: "previous_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.cellbroadcastreceiver"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.cellbroadcastreceiver"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 3
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.connectivitythermalpowermanager"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.deskclock"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.deskclock"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.deskclock"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.deskclock"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.devicelockcontroller"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.dialer"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.dialer"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 7
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.euicc"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.ext.services"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.flipendo"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.flipendo"
+src_bucket: "previous_app"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.flipendo"
+dest_bucket: "previous_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 5
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms.persistent"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.gms.persistent"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.googlequicksearchbox:search"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.grilservice"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.ims"
+dest_bucket: "persistent_service"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.iwlan"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.keep"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.permissioncontroller"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.permissioncontroller"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.pixelsystemservice"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.providers.media.module"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.providers.media.module"
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.repairmode"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.repairmode"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.repairmode"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.android.youtube"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.ar.core"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.ar.core"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.euiccpixel"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.euiccpixel"
+dest_bucket: "previous_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.google.pixel.camera.services"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.overdrive.mobile.android.libby"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.patreon.android"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.samsung.slsi.telephony.oem.oemrilhookservice"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.samsung.slsi.telephony.oemril"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.shannon.imsservice"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.shannon.rcsservice"
+dest_bucket: "persistent_proc"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.shannon.rcsservice:shannonrcsservice"
+dest_bucket: "previous_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "com.spotify.music"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "logcat"
+dest_bucket: "logcat"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "logcat"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "media.extractor"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "media.metrics"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "media.swcodec"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "org.thoughtcrime.securesms"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "org.videolan.vlc"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "perfetto"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "system_server"
+dest_bucket: "system"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "webview_zygote"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_process {
+name: "zygote64"
+dest_bucket: "native"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "cached_app"
+count: 7
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 8
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "bindService"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 8
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startReceiver"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 7
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "uiVisibility"
+src_bucket: "cached_app"
+dest_bucket: "cached_app"
+count: 3
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "uiVisibility"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app"
+count: 5
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "cached_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "previous_app"
+dest_bucket: "cached_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "cached_app"
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "cached_app_lmk_first"
+dest_bucket: "cached_app_lmk_first"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+src_bucket: "previous_app"
+dest_bucket: "cached_app_lmk_first"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "foreground_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "activityChange"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "processBegin"
+dest_bucket: "foreground_app"
+count: 24
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startReceiver"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "cached_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "perceptible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "unbindService"
+src_bucket: "perceptible_low_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "previous_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "bindService"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "getProvider"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "visible_app"
+dest_bucket: "foreground_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "logcat"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "native"
+count: 117
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+dest_bucket: "native"
+count: 4
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_app"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "perceptible_low_app"
+count: 3
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "persistent_proc"
+count: 13
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "processBegin"
+dest_bucket: "persistent_proc"
+count: 4
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "persistent_service"
+count: 2
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "startService"
+src_bucket: "foreground_app"
+dest_bucket: "persistent_service"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "previous_app"
+count: 5
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "service"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "system"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "unknown_native"
+count: 1
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+dest_bucket: "visible_app"
+count: 6
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "executingService"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 3
+}
+oom_adjuster_transition_counts_by_oom_adj_reason {
+name: "finishReceiver"
+src_bucket: "foreground_app"
+dest_bucket: "visible_app"
+count: 1
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "cached_app"
+total_dur: 20351484924
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "cached_app_lmk_first"
+total_dur: 544761924
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "foreground_app"
+total_dur: 33803455234
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "logcat"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "native"
+total_dur: 263503709570
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "perceptible_app"
+total_dur: 940071238
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "perceptible_low_app"
+total_dur: 199288116
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "persistent_proc"
+total_dur: 36752485443
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "persistent_service"
+total_dur: 6049062188
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "previous_app"
+total_dur: 6572285720
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "service"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "system"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "unknown_native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_global {
+bucket: "visible_app"
+total_dur: 12579222928
+}
+oom_adj_bucket_duration_agg_by_process {
+name: ".ShannonImsService"
+bucket: "foreground_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.android.adbd/bin/adbd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.android.hardware.cas/bin/hw/android.hardware.cas-service.example"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.android.os.statsd/bin/statsd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.google.android.hardware.biometrics.face/bin/hw/android.hardware.biometrics.face-service.22.pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.google.android.widevine/bin/hw/android.hardware.drm-service.widevine"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.google.pixel.camera.hal/bin/hw/android.hardware.camera.provider@2.7-service-google"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.google.pixel.camera.hal/bin/rlsservice"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/apex/com.google.pixel.wifi.ext/bin/hw/vendor.google.wifi_ext-service-vendor"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/apexd"
+bucket: "native"
+total_dur: 25397654
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/audioserver"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/cameraserver"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/credstore"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/drmserver"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/gatekeeperd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/gpuservice"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/hw/android.system.suspend-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/incidentd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/init"
+bucket: "native"
+total_dur: 4502600324
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/installd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/ip6tables-restore"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/iptables-restore"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/keystore2"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/lmkd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/logd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/mediaserver"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/netd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/prng_seeder"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/servicemanager"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/storaged"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/surfaceflinger"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/tombstoned"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/traced"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/traced_probes"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/ueventd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/update_engine"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/virtual_camera"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/vold"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/bin/wificond"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/system_ext/bin/hw/android.hidl.allocator@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/system_ext/bin/hwservicemanager"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system/vendor/bin/wfc-pkt-router"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system_ext/bin/gs_watchdogd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/system_ext/bin/hw/vendor.google.edgetpu_app_service@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/aocd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/bipchmgr"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/cbd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/chre"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/dmd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.audio.service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.authsecret-service.citadel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.biometrics.fingerprint-service.goodix"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.bluetooth-service.bcmbtlinux"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.boot-service.default-pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.cas@1.2-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.composer.hwc3-service.pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.confirmationui-service.trusty.vendor"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.contexthub-service.generic"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.drm-service.clearkey"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.dumpstate-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.edgetpu.logging@service-edgetpu-logging"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.gatekeeper-service.trusty"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.gnss@2.1-service-brcm"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.graphics.allocator-V2-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.gxp.logging@service-gxp-logging"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.health-service.gs201"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.identity@1.0-service.citadel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.input.processor-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.memtrack-service.pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.neuralnetworks@service-darwinn-aidl"
+bucket: "native"
+total_dur: 4502600324
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.nfc-service.st"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.oemlock-service.citadel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.power-service.pixel-libperfmgr"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.power.stats-service.pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-service-gto"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-service-gto-ese2"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.secure_element@1.2-uicc-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.security.keymint-service.citadel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.security.keymint-service.trusty"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.usb-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.usb.gadget-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.vibrator-service.cs40l26"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/android.hardware.weaver-service.citadel"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/battery_mitigation"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/citadeld"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/google.hardware.media.c2@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/gpsd"
+bucket: "unknown_native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/lhd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/rild_exynos"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/samsung.hardware.media.c2@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/scd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/vendor.dolby.media.c2@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/vendor.google.audiometricext@1.0-service-vendor"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/vendor.google.google_battery-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/vendor.google.wireless_charger-default"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/hw/wpa_supplicant"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/modem_svc_sit"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/pixelstats-vendor"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/ramdump"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/rfsd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/sced"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/securedpud.slider"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/sscoredump"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/storageproxyd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/trusty_metricsd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/twoshay"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/vcd"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "/vendor/bin/vndservicemanager"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "<pre-initialized>"
+bucket: "native"
+total_dur: 50795308
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "android.process.acore"
+bucket: "foreground_app"
+total_dur: 823432282
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "android.process.acore"
+bucket: "previous_app"
+total_dur: 1427867880
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.amazon.mp3"
+bucket: "foreground_app"
+total_dur: 1322230378
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.hbmsvmanager"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.keychain"
+bucket: "foreground_app"
+total_dur: 196867340
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.networkstack.process"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.nfc"
+bucket: "persistent_proc"
+total_dur: 1883581900
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.phone"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.pixellogger"
+bucket: "persistent_proc"
+total_dur: 1852027416
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.printspooler"
+bucket: "foreground_app"
+total_dur: 343904124
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.providers.calendar"
+bucket: "foreground_app"
+total_dur: 281386139
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.qns"
+bucket: "visible_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.se"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.settings"
+bucket: "cached_app"
+total_dur: 967052521
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.settings"
+bucket: "foreground_app"
+total_dur: 1284247641
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.android.systemui"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.apple.android.music"
+bucket: "foreground_app"
+total_dur: 1268519522
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.blinkslabs.blinkist.android"
+bucket: "foreground_app"
+total_dur: 1250421540
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.SSRestartDetector"
+bucket: "persistent_proc"
+total_dur: 1885479849
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.books"
+bucket: "foreground_app"
+total_dur: 1076963328
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.cbrsnetworkmonitor"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "cached_app"
+total_dur: 1086183503
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "foreground_app"
+total_dur: 402609904
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.dreamliner"
+bucket: "perceptible_low_app"
+total_dur: 7658935
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging"
+bucket: "native"
+total_dur: 25397654
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging:rcs"
+bucket: "foreground_app"
+total_dur: 991455486
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.messaging:rcs"
+bucket: "perceptible_app"
+total_dur: 940071238
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.nexuslauncher"
+bucket: "foreground_app"
+total_dur: 1661132477
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.nexuslauncher"
+bucket: "visible_app"
+total_dur: 590167685
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.scone"
+bucket: "service"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wallpaper"
+bucket: "foreground_app"
+total_dur: 177919993
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wellbeing"
+bucket: "cached_app"
+total_dur: 1366329916
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.wellbeing"
+bucket: "foreground_app"
+total_dur: 884970246
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.work.clouddpc"
+bucket: "cached_app"
+total_dur: 2194632641
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.work.clouddpc"
+bucket: "cached_app_lmk_first"
+total_dur: 56667521
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.apps.youtube.music"
+bucket: "foreground_app"
+total_dur: 1558635529
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as"
+bucket: "foreground_app"
+total_dur: 2003700552
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "cached_app"
+total_dur: 621111806
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "foreground_app"
+total_dur: 303909018
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.as.oss"
+bucket: "perceptible_low_app"
+total_dur: 9431030
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.bluetooth"
+bucket: "persistent_service"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.calendar"
+bucket: "foreground_app"
+total_dur: 1573637523
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.carrier"
+bucket: "cached_app"
+total_dur: 870239329
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.carrier"
+bucket: "previous_app"
+total_dur: 1381060833
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.cellbroadcastreceiver"
+bucket: "cached_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.connectivitythermalpowermanager"
+bucket: "persistent_proc"
+total_dur: 1864494172
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.deskclock"
+bucket: "cached_app"
+total_dur: 389354777
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.deskclock"
+bucket: "cached_app_lmk_first"
+total_dur: 187275391
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.deskclock"
+bucket: "foreground_app"
+total_dur: 1674669994
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "cached_app"
+total_dur: 510497641
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "cached_app_lmk_first"
+total_dur: 56693278
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "foreground_app"
+total_dur: 115664632
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.devicelockcontroller"
+bucket: "visible_app"
+total_dur: 1568444611
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.dialer"
+bucket: "cached_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.euicc"
+bucket: "visible_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.ext.services"
+bucket: "visible_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.flipendo"
+bucket: "cached_app"
+total_dur: 2063730581
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.flipendo"
+bucket: "cached_app_lmk_first"
+total_dur: 56672527
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.flipendo"
+bucket: "previous_app"
+total_dur: 130897054
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "cached_app"
+total_dur: 594464397
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "foreground_app"
+total_dur: 1259805736
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms"
+bucket: "visible_app"
+total_dur: 397030029
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms.persistent"
+bucket: "foreground_app"
+total_dur: 2037190298
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.gms.persistent"
+bucket: "visible_app"
+total_dur: 214109864
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+bucket: "foreground_app"
+total_dur: 1185413249
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.googlequicksearchbox:interactor"
+bucket: "visible_app"
+total_dur: 804270091
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.googlequicksearchbox:search"
+bucket: "foreground_app"
+total_dur: 1467442984
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.grilservice"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.ims"
+bucket: "persistent_service"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.iwlan"
+bucket: "visible_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.keep"
+bucket: "foreground_app"
+total_dur: 576153188
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.permissioncontroller"
+bucket: "cached_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.pixelsystemservice"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.providers.media.module"
+bucket: "foreground_app"
+total_dur: 518140177
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.providers.media.module"
+bucket: "persistent_service"
+total_dur: 1546461864
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.repairmode"
+bucket: "cached_app"
+total_dur: 2063846955
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.repairmode"
+bucket: "cached_app_lmk_first"
+total_dur: 187453207
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.android.youtube"
+bucket: "foreground_app"
+total_dur: 1450400991
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.ar.core"
+bucket: "foreground_app"
+total_dur: 733794515
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.ar.core"
+bucket: "perceptible_low_app"
+total_dur: 182198151
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.euiccpixel"
+bucket: "cached_app"
+total_dur: 870140371
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.euiccpixel"
+bucket: "previous_app"
+total_dur: 1381159791
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.google.pixel.camera.services"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.overdrive.mobile.android.libby"
+bucket: "foreground_app"
+total_dur: 889039012
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.patreon.android"
+bucket: "foreground_app"
+total_dur: 608530141
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.samsung.slsi.telephony.oem.oemrilhookservice"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.samsung.slsi.telephony.oemril"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.shannon.imsservice"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.shannon.rcsservice"
+bucket: "persistent_proc"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.shannon.rcsservice:shannonrcsservice"
+bucket: "previous_app"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "com.spotify.music"
+bucket: "foreground_app"
+total_dur: 566682160
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "logcat"
+bucket: "logcat"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "logcat"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "media.extractor"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "media.metrics"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "media.swcodec"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "org.thoughtcrime.securesms"
+bucket: "foreground_app"
+total_dur: 701758738
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "org.videolan.vlc"
+bucket: "foreground_app"
+total_dur: 361526235
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "perfetto"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "system_server"
+bucket: "system"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "webview_zygote"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_bucket_duration_agg_by_process {
+name: "zygote64"
+bucket: "native"
+total_dur: 2251300162
+}
+oom_adj_duration_agg {
+oom_adj_event_count: 0
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 263550
+max_oom_adj_dur: 7292196
+avg_oom_adj_dur: 3888985.6470588236
+oom_adj_event_count: 4
+oom_adj_reason: "activityChange"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 67180
+max_oom_adj_dur: 225871
+avg_oom_adj_dur: 171359.88888888888
+oom_adj_event_count: 6
+oom_adj_reason: "bindService"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 64657
+max_oom_adj_dur: 565674
+avg_oom_adj_dur: 232660.2142857143
+oom_adj_event_count: 11
+oom_adj_reason: "executingService"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 198120
+max_oom_adj_dur: 198120
+avg_oom_adj_dur: 198120.0
+oom_adj_event_count: 1
+oom_adj_reason: "finishReceiver"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 193766
+max_oom_adj_dur: 626302
+avg_oom_adj_dur: 383901.0
+oom_adj_event_count: 4
+oom_adj_reason: "getProvider"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 92366
+max_oom_adj_dur: 1461914
+avg_oom_adj_dur: 330079.5714285714
+oom_adj_event_count: 28
+oom_adj_reason: "processBegin"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 238525
+max_oom_adj_dur: 5160522
+avg_oom_adj_dur: 883870.1111111111
+oom_adj_event_count: 4
+oom_adj_reason: "startReceiver"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 68645
+max_oom_adj_dur: 2294270
+avg_oom_adj_dur: 618880.2
+oom_adj_event_count: 5
+oom_adj_reason: "startService"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 10092
+max_oom_adj_dur: 10092
+avg_oom_adj_dur: 10092.0
+oom_adj_event_count: 1
+oom_adj_reason: "uiVisibility"
+}
+oom_adj_duration_agg {
+min_oom_adj_dur: 165242
+max_oom_adj_dur: 380290
+avg_oom_adj_dur: 272766.0
+oom_adj_event_count: 2
+oom_adj_reason: "unbindService"
+}
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index 06cbe59..da5559e 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -303,3 +303,9 @@
          }
        }
        """))
+  def test_android_oom_adjuster(self):
+    return DiffTestBlueprint(
+      trace=DataPath('android_postboot_unlock.pftrace'),
+      query=Metric("android_oom_adjuster"),
+      out=Path('android_oom_adjuster.out')
+    )
diff --git a/test/trace_processor/diff_tests/parser/android/protolog_packet_loss.textproto b/test/trace_processor/diff_tests/parser/android/protolog_packet_loss.textproto
new file mode 100644
index 0000000..ab6fda5
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/android/protolog_packet_loss.textproto
@@ -0,0 +1,118 @@
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  sequence_flags: 1
+  previous_packet_dropped: true
+  trusted_pid: 1716
+  first_packet_on_sequence: true
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  interned_data {
+    protolog_string_args {
+      iid: 1
+      str: "MyTestString"
+    }
+  }
+  trusted_pid: 1716
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  interned_data {
+    protolog_string_args {
+      iid: 2
+      str: "MyOtherTestString"
+    }
+  }
+  trusted_pid: 1716
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  sequence_flags: 2
+  trusted_pid: 1716
+  timestamp: 857384130
+  protolog_message {
+    message_id: 6924537961316301726
+    str_param_iids: 1
+    str_param_iids: 1
+  }
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  sequence_flags: 2
+  trusted_pid: 1716
+  timestamp: 857384130
+  protolog_message {
+    message_id: 6924537961316301726
+    str_param_iids: 1
+    str_param_iids: 2
+  }
+  previous_packet_dropped: true
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 3
+  sequence_flags: 2
+  trusted_pid: 1716
+  timestamp: 857384130
+  protolog_message {
+    message_id: 6924537961316301726
+    str_param_iids: 2
+    str_param_iids: 2
+  }
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 4
+  sequence_flags: 1
+  previous_packet_dropped: true
+  trusted_pid: 1716
+  first_packet_on_sequence: true
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 4
+  interned_data {
+    protolog_string_args {
+      iid: 1
+      str: "MyNextTestString"
+    }
+  }
+  trusted_pid: 1716
+}
+packet {
+  trusted_uid: 1000
+  trusted_packet_sequence_id: 4
+  sequence_flags: 2
+  trusted_pid: 1716
+  timestamp: 857384130
+  protolog_message {
+    message_id: 6924537961316301726
+    str_param_iids: 1
+    str_param_iids: 1
+  }
+}
+packet {
+  trusted_uid: 10224
+  trusted_packet_sequence_id: 10
+  previous_packet_dropped: true
+  trusted_pid: 2063
+  first_packet_on_sequence: true
+  protolog_viewer_config {
+    messages {
+      message_id: 6924537961316301726
+      message: "Test message with two strings: %s and %s"
+      level: PROTOLOG_LEVEL_DEBUG
+      group_id: 1
+    }
+    groups {
+      id: 1
+      name: "MY_FIRST_GROUP"
+      tag: "MyFirstGroup"
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/parser/android/tests.py b/test/trace_processor/diff_tests/parser/android/tests.py
index 9fca28e..6a20f59 100644
--- a/test/trace_processor/diff_tests/parser/android/tests.py
+++ b/test/trace_processor/diff_tests/parser/android/tests.py
@@ -15,7 +15,7 @@
 
 from python.generators.diff_tests.testing import Path, DataPath, Metric, Systrace
 from python.generators.diff_tests.testing import Csv, Json, TextProto, BinaryProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 from python.generators.diff_tests.testing import PrintProfileProto
 
@@ -203,3 +203,54 @@
       0
       0
       """))
+
+  # Tests when counter_tack.machine_id is not null.
+  def test_android_system_property_counter_machine_id(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          timestamp: 1000
+          android_system_property {
+            values {
+              name: "debug.tracing.screen_state"
+              value: "2"
+            }
+            values {
+              name: "debug.tracing.device_state"
+              value: "some_state_from_sysprops"
+            }
+          }
+          machine_id: 1001
+        }
+        packet {
+          ftrace_events {
+            cpu: 1
+            event {
+              timestamp: 2000
+              pid: 1
+              print {
+                buf: "C|1000|ScreenState|1\n"
+              }
+            }
+            event {
+              timestamp: 3000
+              pid: 1
+              print {
+                buf: "N|1000|DeviceStateChanged|some_state_from_atrace\n"
+              }
+            }
+          }
+          machine_id: 1001
+        }
+        """),
+        query="""
+        SELECT t.type, t.name, c.id, c.ts, c.type, c.value
+        FROM counter_track t JOIN counter c ON t.id = c.track_id
+        WHERE name = 'ScreenState'
+          AND t.machine_id IS NOT NULL;
+        """,
+        out=Csv("""
+        "type","name","id","ts","type","value"
+        "counter_track","ScreenState",0,1000,"counter",2.000000
+        "counter_track","ScreenState",1,2000,"counter",1.000000
+        """))
diff --git a/test/trace_processor/diff_tests/parser/android/tests_protolog.py b/test/trace_processor/diff_tests/parser/android/tests_protolog.py
index 404f34e..358fe51 100644
--- a/test/trace_processor/diff_tests/parser/android/tests_protolog.py
+++ b/test/trace_processor/diff_tests/parser/android/tests_protolog.py
@@ -31,3 +31,13 @@
         1,857384110,"WARN","MySecondGroup","Test message with different int formats: 1776, 0o3360, 0x6f0, 888.000000, 8.880000e+02.","[NULL]"
         2,857384130,"ERROR","MyThirdGroup","Message re-using interned string 'MyOtherTestString' == 'MyOtherTestString', but 'SomeOtherTestString' != 'MyOtherTestString'","[NULL]"
         """))
+
+  def test_handles_packet_loss(self):
+    return DiffTestBlueprint(
+        trace=Path('protolog_packet_loss.textproto'),
+        query="SELECT id, ts, level, tag, message, stacktrace FROM protolog;",
+        out=Csv("""
+        "id","ts","level","tag","message","stacktrace"
+        0,857384130,"DEBUG","MyFirstGroup","Test message with two strings: MyTestString and MyTestString","[NULL]"
+        1,857384130,"DEBUG","MyFirstGroup","Test message with two strings: MyNextTestString and MyNextTestString","[NULL]"
+        """))
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests.py b/test/trace_processor/diff_tests/parser/graphics/tests.py
index 7fe4af1..0d5d8b5 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -296,3 +296,20 @@
   #      751796307210,4313965,"mali_KCPU_FENCE_WAIT"
   #      751800638997,0,"mali_KCPU_FENCE_SIGNAL"
   #      """))
+
+  # Tests gpu_track with machine_id ID.
+  def test_graphics_frame_events_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('graphics_frame_events.py'),
+        trace_modifier=TraceInjector(['graphics_frame_event'],
+                                     {'machine_id': 1001}),
+        query="""
+        SELECT ts, gpu_track.name AS track_name, dur, frame_slice.name AS slice_name,
+          frame_number, layer_name
+        FROM gpu_track
+        LEFT JOIN frame_slice ON gpu_track.id = frame_slice.track_id
+        WHERE scope = 'graphics_frame_event'
+          AND gpu_track.machine_id IS NOT NULL
+        ORDER BY ts;
+        """,
+        out=Path('graphics_frame_events.out'))
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index 8619bc3..505bdf9 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -1354,3 +1354,58 @@
         "name","severity","value"
         "ftrace_abi_errors_skipped_zero_data_length","info",1
         """))
+
+  # CPU info
+  def test_cpu_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('cpu_info.textproto'),
+        trace_modifier=TraceInjector(['cpu_info'], {'machine_id': 1001}),
+        query="""
+        SELECT
+          id,
+          cluster_id,
+          processor
+        FROM cpu
+        WHERE machine_id is not NULL;
+        """,
+        out=Csv("""
+        "id","cluster_id","processor"
+        0,0,"AArch64 Processor rev 13 (aarch64)"
+        1,0,"AArch64 Processor rev 13 (aarch64)"
+        2,0,"AArch64 Processor rev 13 (aarch64)"
+        3,0,"AArch64 Processor rev 13 (aarch64)"
+        4,0,"AArch64 Processor rev 13 (aarch64)"
+        5,0,"AArch64 Processor rev 13 (aarch64)"
+        6,1,"AArch64 Processor rev 13 (aarch64)"
+        7,1,"AArch64 Processor rev 13 (aarch64)"
+        """))
+
+  def test_cpu_freq_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('cpu_info.textproto'),
+        trace_modifier=TraceInjector(['cpu_info'], {'machine_id': 1001}),
+        query="""
+        SELECT
+          freq,
+          GROUP_CONCAT(cpu_id) AS cpus
+        FROM cpu_freq
+        WHERE machine_id is not NULL
+        GROUP BY freq
+        ORDER BY freq;
+        """,
+        out=Path('cpu_freq.out'))
+
+  def test_sched_waking_instants_compact_sched_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('compact_sched.pb'),
+        trace_modifier=TraceInjector(
+            ['ftrace_events', 'ftrace_stats', 'system_info'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT ts, thread.name, thread.tid
+        FROM thread_state
+        JOIN thread USING (utid)
+        WHERE state = 'R' AND thread_state.machine_id is not NULL
+        ORDER BY ts;
+        """,
+        out=Path('sched_waking_instants_compact_sched.out'))
diff --git a/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py b/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
index f62ba2c..d678400 100644
--- a/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
+++ b/test/trace_processor/diff_tests/parser/power/tests_energy_breakdown.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -90,3 +90,35 @@
         3,10190
         3,10235
         """))
+
+  def test_energy_breakdown_uid_table_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('energy_breakdown_uid.textproto'),
+        trace_modifier=TraceInjector(['android_energy_estimation_breakdown'],
+                                     {'machine_id': 1001}),
+        query="""
+        SELECT uid, name
+        FROM uid_counter_track
+        WHERE machine_id IS NOT NULL;
+        """,
+        out=Csv("""
+        "uid","name"
+        10234,"GPU"
+        10190,"GPU"
+        10235,"GPU"
+        """))
+
+  def test_energy_breakdown_table_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('energy_breakdown.textproto'),
+        trace_modifier=TraceInjector(['android_energy_estimation_breakdown'],
+                                     {'machine_id': 1001}),
+        query="""
+        SELECT consumer_id, name, consumer_type, ordinal
+        FROM energy_counter_track
+        WHERE machine_id IS NOT NULL;
+        """,
+        out=Csv("""
+        "consumer_id","name","consumer_type","ordinal"
+        0,"CPUCL0","CPU_CLUSTER",0
+        """))
diff --git a/test/trace_processor/diff_tests/parser/process_tracking/tests.py b/test/trace_processor/diff_tests/parser/process_tracking/tests.py
index 9092541..b2a04a7 100644
--- a/test/trace_processor/diff_tests/parser/process_tracking/tests.py
+++ b/test/trace_processor/diff_tests/parser/process_tracking/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -227,6 +227,36 @@
         19999,"[NULL]","[NULL]","real_name"
         """))
 
+  def test_process_tracking_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('synth_process_tracking.py'),
+        trace_modifier=TraceInjector(['ftrace_events', 'process_tree'],
+                                     {'machine_id': 1001}),
+        query="""
+        SELECT tid, pid, process.name AS pname, thread.name AS tname,
+               thread.machine_id as tmachine, process.machine_id as pmachine
+        FROM thread
+        LEFT JOIN process USING(upid)
+        WHERE tid > 0
+        ORDER BY tid;
+        """,
+        out=Csv("""
+        "tid","pid","pname","tname","tmachine","pmachine"
+        10,10,"process1","p1-t0",1,1
+        11,"[NULL]","[NULL]","p1-t1",1,"[NULL]"
+        12,10,"process1","p1-t2",1,1
+        20,20,"process_2","p2-t0",1,1
+        21,20,"process_2","p2-t1",1,1
+        22,20,"process_2","p2-t2",1,1
+        30,30,"process_3","p3-t0",1,1
+        31,30,"process_3","p3-t1",1,1
+        31,40,"process_4","p4-t1",1,1
+        32,30,"process_3","p3-t2",1,1
+        33,30,"process_3","p3-t3",1,1
+        34,30,"process_3","p3-t4",1,1
+        40,40,"process_4","p4-t0",1,1
+        """))
+
   def test_process_stats_process_runtime(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
diff --git a/test/trace_processor/diff_tests/parser/sched/tests.py b/test/trace_processor/diff_tests/parser/sched/tests.py
index ea04126..5568147 100644
--- a/test/trace_processor/diff_tests/parser/sched/tests.py
+++ b/test/trace_processor/diff_tests/parser/sched/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -72,4 +72,39 @@
         "Cpu 5 Util",13000,125.000000
         "Cpu 5 Cap",13000,757.000000
         "Cpu 5 Nr Running",13000,1.000000
-        """))
\ No newline at end of file
+        """))
+
+  def test_sched_cpu_util_cfs_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('sched_cpu_util_cfs.textproto'),
+        trace_modifier=TraceInjector(['ftrace_events'], {'machine_id': 1001}),
+        query="""
+        SELECT
+          t.name,
+          c.ts,
+          c.value,
+          c.machine_id
+        FROM
+          counter AS c
+        LEFT JOIN
+          counter_track AS t
+          ON c.track_id = t.id
+        WHERE
+          name GLOB "Cpu ? Cap" OR name GLOB "Cpu ? Util" OR name GLOB "Cpu ? Nr Running"
+        ORDER BY ts;
+        """,
+        out=Csv("""
+        "name","ts","value","machine_id"
+        "Cpu 6 Util",10000,1.000000,1
+        "Cpu 6 Cap",10000,1004.000000,1
+        "Cpu 6 Nr Running",10000,0.000000,1
+        "Cpu 7 Util",11000,1.000000,1
+        "Cpu 7 Cap",11000,1007.000000,1
+        "Cpu 7 Nr Running",11000,0.000000,1
+        "Cpu 4 Util",12000,43.000000,1
+        "Cpu 4 Cap",12000,760.000000,1
+        "Cpu 4 Nr Running",12000,0.000000,1
+        "Cpu 5 Util",13000,125.000000,1
+        "Cpu 5 Cap",13000,757.000000,1
+        "Cpu 5 Nr Running",13000,1.000000,1
+        """))
diff --git a/test/trace_processor/diff_tests/parser/track_event/tests.py b/test/trace_processor/diff_tests/parser/track_event/tests.py
index 363b907..d71a2fa 100644
--- a/test/trace_processor/diff_tests/parser/track_event/tests.py
+++ b/test/trace_processor/diff_tests/parser/track_event/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -692,3 +692,70 @@
         12000,"slice3"
         13000,"slice4"
         """))
+
+  def test_track_event_tracks_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('track_event_tracks.textproto'),
+        trace_modifier=TraceInjector(['track_descriptor', 'track_event'],
+                                     {'machine_id': 1001}),
+        query="""
+        WITH track_with_name AS (
+          SELECT
+            COALESCE(
+              t1.name,
+              'thread=' || thread.name,
+              'process=' || process.name,
+              'tid=' || thread.tid,
+              'pid=' || process.pid
+            ) AS full_name,
+            *
+          FROM track t1
+          LEFT JOIN thread_track t2 USING (id)
+          LEFT JOIN thread USING (utid)
+          LEFT JOIN process_track t3 USING (id)
+          LEFT JOIN process ON t3.upid = process.id
+          WHERE t1.machine_id IS NOT NULL
+          ORDER BY id
+        )
+        SELECT t1.full_name AS name, t2.full_name AS parent_name,
+               EXTRACT_ARG(t1.source_arg_set_id, 'has_first_packet_on_sequence')
+               AS has_first_packet_on_sequence
+        FROM track_with_name t1
+        LEFT JOIN track_with_name t2 ON t1.parent_id = t2.id
+        ORDER BY 1, 2;
+        """,
+        out=Csv("""
+        "name","parent_name","has_first_packet_on_sequence"
+        "Default Track","[NULL]","[NULL]"
+        "async","process=p1",1
+        "async2","process=p1",1
+        "async3","thread=t2",1
+        "event_and_track_async3","process=p1",1
+        "process=p1","[NULL]","[NULL]"
+        "process=p2","[NULL]","[NULL]"
+        "process=p2","[NULL]","[NULL]"
+        "thread=t1","process=p1",1
+        "thread=t2","process=p1",1
+        "thread=t3","process=p1",1
+        "thread=t4","process=p2","[NULL]"
+        "tid=1","[NULL]","[NULL]"
+        """))
+
+  # Tests thread_counter_track.machine_id is not null.
+  def test_track_event_counters_counters_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('track_event_counters.textproto'),
+        trace_modifier=TraceInjector(
+            ['track_descriptor', 'track_event', 'trace_packet_defaults'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT type, name, machine_id
+        FROM thread_counter_track
+        WHERE machine_id IS NOT NULL
+        """,
+        out=Csv("""
+        "type","name","machine_id"
+        "thread_counter_track","thread_time",1
+        "thread_counter_track","thread_time",1
+        "thread_counter_track","thread_instruction_count",1
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/OWNERS b/test/trace_processor/diff_tests/stdlib/OWNERS
index 0479e4b..21e80a4 100644
--- a/test/trace_processor/diff_tests/stdlib/OWNERS
+++ b/test/trace_processor/diff_tests/stdlib/OWNERS
@@ -1,3 +1,3 @@
-# These can't be added inside chrome/ bacause the whole directory is
+# These can't be added inside chrome/ because the whole directory is
 # blown away on every import.
 per-file chrome/* = file://protos/third_party/CHROMIUM_OWNERS
diff --git a/test/trace_processor/diff_tests/stdlib/android/frames_tests.py b/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
index eb6e6d0..83297fd 100644
--- a/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/frames_tests.py
@@ -30,22 +30,22 @@
         SELECT * FROM android_frames_choreographer_do_frame;
         """,
         out=Csv("""
-        "id","frame_id","ui_thread_utid"
-        2,10,2
-        15,20,2
-        22,30,2
-        35,40,2
-        46,60,2
-        55,90,2
-        63,100,2
-        73,110,2
-        79,120,2
-        87,130,2
-        93,140,2
-        99,145,2
-        102,150,2
-        108,160,2
-        140,1000,2
+        "id","frame_id","ui_thread_utid","upid"
+        2,10,2,2
+        15,20,2,2
+        22,30,2,2
+        35,40,2,2
+        46,60,2,2
+        55,90,2,2
+        63,100,2,2
+        73,110,2,2
+        79,120,2,2
+        87,130,2,2
+        93,140,2,2
+        99,145,2,2
+        102,150,2,2
+        108,160,2,2
+        140,1000,2,2
         """))
 
   def test_android_frames_draw_frame(self):
@@ -57,24 +57,24 @@
         SELECT * FROM android_frames_draw_frame;
         """,
         out=Csv("""
-        "id","frame_id","render_thread_utid"
-        8,10,4
-        16,20,4
-        23,30,4
-        41,40,4
-        50,60,4
-        57,90,4
-        60,90,4
-        66,100,4
-        69,100,4
-        74,110,4
-        80,120,4
-        89,130,4
-        95,140,4
-        100,145,4
-        105,150,4
-        109,160,4
-        146,1000,4
+        "id","frame_id","render_thread_utid","upid"
+        8,10,4,2
+        16,20,4,2
+        23,30,4,2
+        41,40,4,2
+        50,60,4,2
+        57,90,4,2
+        60,90,4,2
+        66,100,4,2
+        69,100,4,2
+        74,110,4,2
+        80,120,4,2
+        89,130,4,2
+        95,140,4,2
+        100,145,4,2
+        105,150,4,2
+        109,160,4,2
+        146,1000,4,2
         """))
 
   def test_android_frames(self):
diff --git a/test/trace_processor/diff_tests/stdlib/android/startups_tests.py b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
new file mode 100644
index 0000000..197ac2a
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/android/startups_tests.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      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.
+
+from python.generators.diff_tests.testing import Path, DataPath
+from python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class Startups(TestSuite):
+
+  def test_hot_startups(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_hot.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,186969441973689,186969489302704,47329015,"androidx.benchmark.integration.macrobenchmark.target","[NULL]"
+        """))
+
+  def test_warm_startups(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_warm.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,186982050780778,186982115528805,64748027,"androidx.benchmark.integration.macrobenchmark.target","[NULL]"
+        """))
+
+  def test_cold_startups(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_cold.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,186974938196632,186975083989042,145792410,"androidx.benchmark.integration.macrobenchmark.target","[NULL]"
+        """))
+
+  def test_hot_startups_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_hot.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,779860286416,779893485322,33198906,"com.google.android.googlequicksearchbox","hot"
+        2,780778904571,780813944498,35039927,"androidx.benchmark.integration.macrobenchmark.target","hot"
+        """))
+
+  def test_warm_startups_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_warm.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,799979565075,800014194731,34629656,"com.google.android.googlequicksearchbox","hot"
+        2,800868511677,800981929562,113417885,"androidx.benchmark.integration.macrobenchmark.target","[NULL]"
+        """))
+
+  def test_cold_startups_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_cold.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.startups;
+        SELECT * FROM android_startups;
+        """,
+        out=Csv("""
+        "startup_id","ts","ts_end","dur","package","startup_type"
+        1,791231114368,791501060868,269946500,"androidx.benchmark.integration.macrobenchmark.target","[NULL]"
+        """))
+
+  def test_android_startup_time_to_display_hot_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_hot.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,33198906,"[NULL]",1,"[NULL]",355
+        2,35039927,537343160,4,5,383
+        """))
+
+  def test_android_startup_time_to_display_warm_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_warm.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,34629656,"[NULL]",1,"[NULL]",355
+        2,108563770,581026583,4,5,388
+        """))
+
+  def test_android_startup_time_to_display_cold_maxsdk28(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api24_startup_cold.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,264869885,715406822,65,66,396
+        """))
+
+  def test_android_startup_time_to_display_hot(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_hot.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,40534066,542222554,5872867,5872953,184
+        """))
+
+  def test_android_startup_time_to_display_warm(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_warm.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,62373965,555968701,5873800,5873889,185
+        """))
+
+  def test_android_startup_time_to_display_cold(self):
+    return DiffTestBlueprint(
+        trace=DataPath('api31_startup_cold.perfetto-trace'),
+        query="""
+        INCLUDE PERFETTO MODULE android.startup.time_to_display;
+        SELECT * FROM android_startup_time_to_display;
+        """,
+        out=Csv("""
+        "startup_id","time_to_inital_display","time_to_full_display","ttid_frame_id","ttfd_frame_id","upid"
+        1,143980066,620815843,5873276,5873353,229
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
index ae641d4..bdde242 100755
--- a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
@@ -40,7 +40,7 @@
 
   def test_chrome_scrolls(self):
     return DiffTestBlueprint(
-        trace=Path('chrome_scroll_check.py'),
+        trace=DataPath('chrome_input_with_frame_view.pftrace'),
         query="""
         INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
 
@@ -55,15 +55,15 @@
         """,
         out=Csv("""
         "id","ts","dur","gesture_scroll_begin_ts","gesture_scroll_end_ts"
-        5678,0,55000000,0,45000000
-        5679,60000000,40000000,60000000,90000000
-        5680,80000000,30000000,80000000,100000000
-        5681,120000000,70000000,120000000,"[NULL]"
+        4328,1035865535981926,1255745000,1035865535981926,1035866753550926
+        4471,1035866799527926,1358505000,1035866799527926,1035868108723926
+        4620,1035868146266926,111786000,1035868146266926,1035868230937926
+        4652,1035868607429926,1517121000,1035868607429926,1035870086449926
         """))
 
   def test_chrome_scroll_intervals(self):
     return DiffTestBlueprint(
-        trace=Path('chrome_scroll_check.py'),
+        trace=DataPath('chrome_input_with_frame_view.pftrace'),
         query="""
         INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
 
@@ -76,14 +76,14 @@
         """,
         out=Csv("""
         "id","ts","dur"
-        1,0,55000000
-        2,60000000,50000000
-        3,120000000,70000000
+        1,1035865535981926,1255745000
+        2,1035866799527926,1458525000
+        3,1035868607429926,1517121000
         """))
 
   def test_chrome_scroll_input_offsets(self):
     return DiffTestBlueprint(
-        trace=DataPath('scroll_offsets.pftrace'),
+        trace=DataPath('scroll_offsets_trace_2.pftrace'),
         query="""
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
 
@@ -91,18 +91,19 @@
           scroll_update_id,
           ts,
           delta_y,
-          offset_y
+          relative_offset_y
         FROM chrome_scroll_input_offsets
+        WHERE scroll_update_id IS NOT NULL
         ORDER by ts
         LIMIT 5;
         """,
         out=Csv("""
-        "scroll_update_id","ts","delta_y","offset_y"
-        1983,4687296612739,-36.999939,-36.999939
-        1983,4687307175845,-39.000092,-76.000031
-        1987,4687313206739,-35.999969,-112.000000
-        1987,4687323152462,-35.000000,-147.000000
-        1991,4687329240739,-28.999969,-175.999969
+        "scroll_update_id","ts","delta_y","relative_offset_y"
+        130,1349914859791,-6.932281,-308.342704
+        132,1349923327791,-32.999954,-341.342659
+        134,1349931893791,-39.999954,-381.342613
+        140,1349956886791,-51.000046,-432.342659
+        147,1349982489791,-89.808540,-522.151199
         """))
 
   def test_chrome_janky_event_latencies_v3(self):
@@ -167,10 +168,10 @@
         """,
         out=Csv("""
         "scroll_id","missed_vsyncs","frame_count","presented_frame_count","janky_frame_count","janky_frame_percent"
-        4328,"[NULL]",109,110,0,0.000000
-        4471,"[NULL]",117,118,0,0.000000
-        4620,"[NULL]",5,4,0,0.000000
-        4652,1,122,122,1,0.820000
+        4328,"[NULL]",110,110,0,0.000000
+        4471,"[NULL]",118,118,0,0.000000
+        4620,"[NULL]",6,4,0,0.000000
+        4652,1,123,122,1,0.820000
         """))
 
   def test_chrome_scroll_jank_intervals_v3(self):
@@ -192,7 +193,7 @@
         """))
   def test_chrome_presented_scroll_offsets(self):
     return DiffTestBlueprint(
-        trace=DataPath('scroll_offsets.pftrace'),
+        trace=DataPath('scroll_offsets_trace_2.pftrace'),
         query="""
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
 
@@ -200,18 +201,19 @@
           scroll_update_id,
           ts,
           delta_y,
-          offset_y
+          relative_offset_y
         FROM chrome_presented_scroll_offsets
+        WHERE scroll_update_id IS NOT NULL
         ORDER by ts
         LIMIT 5;
         """,
         out=Csv("""
-        "scroll_update_id","ts","delta_y","offset_y"
-        1983,4687341817739,"[NULL]",0
-        1987,4687352950739,-50,-50
-        1991,4687364083739,-50,-100
-        1993,4687375224739,-81,-181
-        1996,4687386343739,-66,-247
+        "scroll_update_id","ts","delta_y","relative_offset_y"
+        130,1349963342791,-6.932281,-6.932281
+        132,1349985554791,-16.573090,-23.505371
+        134,1349996680791,-107.517273,-131.022644
+        140,1350007850791,-158.728424,-289.751068
+        147,1350018935791,-89.808540,-379.559608
         """))
 
   def test_scroll_jank_cause_map(self):
diff --git a/test/trace_processor/diff_tests/stdlib/memory/heap_graph_dominator_tree_tests.py b/test/trace_processor/diff_tests/stdlib/memory/heap_graph_dominator_tree_tests.py
index 52bb465..0aa0f64 100644
--- a/test/trace_processor/diff_tests/stdlib/memory/heap_graph_dominator_tree_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/memory/heap_graph_dominator_tree_tests.py
@@ -65,7 +65,7 @@
           19,14,1,128,4,"U"
           20,14,1,256,4,"V"
           21,14,1,512,4,"W"
-          23,25,1,1024,1,"java.lang.ref.FinalizerReference"
+          23,25,1,1024,1,"sun.misc.Cleaner"
         """))
 
   def test_heap_graph_super_root_fn(self):
diff --git a/test/trace_processor/diff_tests/stdlib/memory/heap_graph_for_dominator_tree.textproto b/test/trace_processor/diff_tests/stdlib/memory/heap_graph_for_dominator_tree.textproto
index b0de072..940b38f 100644
--- a/test/trace_processor/diff_tests/stdlib/memory/heap_graph_for_dominator_tree.textproto
+++ b/test/trace_processor/diff_tests/stdlib/memory/heap_graph_for_dominator_tree.textproto
@@ -26,7 +26,7 @@
   # 2. a synthetic tree whose dominator tree is itself. It's drawn below with
   #    each object represented by it's class name. Number in the bracket is the
   #    size of each node in bytes.
-  #                 S[1]     java.lang.ref.FinalizerReference[1024]
+  #                 S[1]     sun.misc.Cleaner[1024]
   #                /    \    /
   #            M[2]      N[4]
   #           /   \      /   \
@@ -191,7 +191,7 @@
     }
     objects {
       id: 0x18
-      type_id: 24 # "java.lang.ref.FinalizerReference"
+      type_id: 24 # "sun.misc.Cleaner"
       self_size: 1024
       reference_object_id: 0x0e
     }
@@ -335,9 +335,15 @@
     }
     types {
       id: 24
-      class_name: "java.lang.ref.FinalizerReference"
+      class_name: "sun.misc.Cleaner"
+      kind: KIND_PHANTOM_REFERENCE
+      reference_field_id: 123
       location_id: 1
     }
+    field_names {
+      iid: 123
+      str: "java.lang.ref.Reference.referent"
+    }
     continued: false
     index: 1
   }
diff --git a/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index eabf714..eef833c 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -414,3 +414,58 @@
           "TO_REALTIME(0)"
           420
         """))
+
+  # Test cpu_track with machine_id ID.
+  def test_cpu_track_table_machine_id(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          ftrace_events {
+            cpu: 1
+            event {
+              timestamp: 100001000000
+              pid: 10
+              irq_handler_entry {
+                irq: 100
+                name : "resource1"
+              }
+            }
+            event {
+              timestamp: 100002000000
+              pid: 10
+              irq_handler_exit {
+                irq: 100
+                ret: 1
+              }
+            }
+          }
+          machine_id: 1001
+        }
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 100003000000
+              pid: 15
+              irq_handler_entry {
+                irq: 100
+                name : "resource1"
+              }
+            }
+          }
+          machine_id: 1001
+        }
+        """),
+        query="""
+        SELECT
+          type,
+          cpu,
+          machine_id
+        FROM cpu_track
+        ORDER BY type, cpu
+        """,
+        out=Csv("""
+        "type","cpu","machine_id"
+        "cpu_track",0,1
+        "cpu_track",1,1
+        """))
diff --git a/test/trace_processor/diff_tests/tables/tests_counters.py b/test/trace_processor/diff_tests/tables/tests_counters.py
index 8ed753f..4cc79a0 100644
--- a/test/trace_processor/diff_tests/tables/tests_counters.py
+++ b/test/trace_processor/diff_tests/tables/tests_counters.py
@@ -15,7 +15,7 @@
 
 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 DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 
 
@@ -125,3 +125,106 @@
         73727335051,23522762
         86726132752,24487554
         """))
+
+  def test_counter_dur_example_android_trace_30s_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        trace_modifier=TraceInjector(
+            ['ftrace_events', 'sys_stats', 'process_stats', 'process_tree'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT ts, dur, machine_id
+        FROM experimental_counter_dur
+        WHERE track_id IN (1, 2, 3)
+        ORDER BY dur LIMIT 10;
+        """,
+        out=Csv("""
+        "ts","dur","machine_id"
+        100351738640,-1,1
+        100351738640,-1,1
+        100351738640,-1,1
+        70731059648,19510835,1
+        70731059648,19510835,1
+        70731059648,19510835,1
+        73727335051,23522762,1
+        73727335051,23522762,1
+        73727335051,23522762,1
+        86726132752,24487554,1
+        """))
+
+  # Tests counter.machine_id and process_counter_track.machine.
+  def test_filter_row_vector_example_android_trace_30s_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        trace_modifier=TraceInjector(
+            ['ftrace_events', 'sys_stats', 'process_stats', 'process_tree'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT ts
+        FROM counter
+        WHERE
+          ts > 72563651549
+          AND track_id = (
+            SELECT t.id
+            FROM process_counter_track t
+            JOIN process p USING (upid)
+            WHERE
+              t.name = 'Heap size (KB)'
+              AND p.pid = 1204
+              AND t.machine_id is not NULL
+          )
+          AND value != 17952.000000
+          AND counter.machine_id is not NULL
+        LIMIT 20;
+        """,
+        out=Path('filter_row_vector_example_android_trace_30s.out'))
+
+  def test_counters_where_cpu_counters_where_cpu_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('counters_where_cpu.py'),
+        trace_modifier=TraceInjector(['ftrace_events'], {'machine_id': 1001}),
+        query="""
+        SELECT
+          ts,
+          lead(ts, 1, ts) OVER (PARTITION BY name ORDER BY ts) - ts AS dur,
+          value, c.machine_id
+        FROM counter c
+        JOIN cpu_counter_track t ON t.id = c.track_id
+        WHERE cpu = 1;
+        """,
+        out=Csv("""
+        "ts","dur","value","machine_id"
+        1000,1,3000.000000,1
+        1001,0,4000.000000,1
+        """))
+
+  def test_synth_1_filter_counter_machine_id(self):
+    return DiffTestBlueprint(
+        trace=Path('../common/synth_1.py'),
+        trace_modifier=TraceInjector(
+            ['ftrace_events', 'process_stats', 'process_tree'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT COUNT(*), machine_id
+        FROM counter
+        WHERE
+          track_id = 0;
+        """,
+        out=Csv("""
+        "COUNT(*)","machine_id"
+        2,1
+        """))
+
+  def test_memory_counters_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('memory_counters.pb'),
+        trace_modifier=TraceInjector(
+            ['ftrace_events', 'sys_stats', 'process_stats', 'process_tree'],
+            {'machine_id': 1001}),
+        query="""
+        SELECT count(*), machine_id FROM counters WHERE -1 < ts group by machine_id;
+        """,
+        out=Csv("""
+        "count(*)","machine_id"
+        98688,1
+        """))
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index 306b8e8..ad23a5d 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -15,7 +15,7 @@
 
 from python.generators.diff_tests.testing import Path, DataPath, Metric
 from python.generators.diff_tests.testing import Csv, Json, TextProto, BinaryProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import DiffTestBlueprint, TraceInjector
 from python.generators.diff_tests.testing import TestSuite
 from python.generators.diff_tests.testing import PrintProfileProto
 
@@ -675,3 +675,38 @@
         thread_state: S (0x0)
         critical path (0x0)
         """))
+
+  # Test machine_id ID of the sched table.
+  def test_android_sched_and_ps_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_sched_and_ps.pb'),
+        trace_modifier=TraceInjector(['ftrace_events'], {'machine_id': 1001}),
+        query="""
+        SELECT ts, cpu, machine_id FROM sched WHERE ts >= 81473797418963 LIMIT 10;
+        """,
+        out=Csv("""
+        "ts","cpu","machine_id"
+        81473797824982,3,1
+        81473797942847,3,1
+        81473798135399,0,1
+        81473798786857,2,1
+        81473798875451,3,1
+        81473799019930,2,1
+        81473799079982,0,1
+        81473800089357,3,1
+        81473800144461,3,1
+        81473800441805,3,1
+        """))
+
+  # Test the support of machine_id ID of the raw table.
+  def test_raw_machine_id(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_sched_and_ps.pb'),
+        trace_modifier=TraceInjector(['ftrace_events'], {'machine_id': 1001}),
+        query="""
+        SELECT count(*) FROM raw WHERE machine_id is NULL;
+        """,
+        out=Csv("""
+        "count(*)"
+        0
+        """))
diff --git a/test/vts/OWNERS b/test/vts/OWNERS
index 10fd9fe..0a7edbb 100644
--- a/test/vts/OWNERS
+++ b/test/vts/OWNERS
@@ -1,6 +1,5 @@
 # Bug component: 323270
 ddiproietto@google.com
-hjd@google.com
 lalitm@google.com
 primiano@google.com
 rsavitski@google.com
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 17f0996..ece5095 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -219,6 +219,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -235,28 +238,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index c8db4ec..c8a2b1a 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -121,13 +121,19 @@
 
 # Proto target groups which will be made public.
 proto_groups = {
-    'trace': [
-        '//protos/perfetto/trace:non_minimal_source_set',
-        '//protos/perfetto/trace:minimal_source_set'
-    ],
-    'config': [
-        '//protos/perfetto/config:source_set',
-    ],
+    'trace': {
+        'types': ['filegroup', 'lite'],
+        'targets': [
+            '//protos/perfetto/trace:non_minimal_source_set',
+            '//protos/perfetto/trace:minimal_source_set',
+        ]
+    },
+    'config': {
+        'types': ['lite'],
+        'targets': [
+            '//protos/perfetto/config:source_set',
+        ]
+    },
 }
 
 needs_libfts = [
@@ -762,7 +768,6 @@
   # The .proto filegroup will be added to `tool_files` of rdeps so that the
   # genrules can be sandboxed.
 
-  tool_files = set()
   for proto_dep in target.proto_deps().union(target.transitive_proto_deps()):
     tool_files.add(":" + label_to_module_name(proto_dep.name))
 
@@ -956,23 +961,34 @@
 
 
 def create_proto_group_modules(blueprint, gn: GnParser, module_name: str,
-                               target_names):
-  # TODO(lalitm): today, we're only adding a Java lite module because that's
-  # the only one used in practice. In the future, if we need other target types
-  # (e.g. C++, Java full etc.) add them here.
-  bp_module_name = label_to_module_name(module_name) + '_java_protos'
-  module = Module('java_library', bp_module_name, bp_module_name)
-  module.comment = f'''GN: [{', '.join(target_names)}]'''
-  module.proto = {'type': 'lite', 'canonical_path_from_root': False}
+                               group):
+  target_names = group['targets']
+  module_types = group['types']
+  module_sources = set()
 
   for name in target_names:
     target = gn.get_target(name)
-    module.srcs.update(gn_utils.label_to_path(src) for src in target.sources)
+    module_sources.update(gn_utils.label_to_path(src) for src in target.sources)
     for dep_label in target.transitive_proto_deps():
       dep = gn.get_target(dep_label.name)
-      module.srcs.update(gn_utils.label_to_path(src) for src in dep.sources)
+      module_sources.update(gn_utils.label_to_path(src) for src in dep.sources)
 
-  blueprint.add_module(module)
+  for type in module_types:
+    if type == 'filegroup':
+      name = label_to_module_name(module_name) + '_filegroup_proto'
+      module = Module('filegroup', name, name)
+      module.comment = f'''GN: [{', '.join(target_names)}]'''
+      module.srcs = module_sources
+      blueprint.add_module(module)
+    elif type == 'lite':
+      name = label_to_module_name(module_name) + '_java_protos'
+      module = Module('java_library', name, name)
+      module.comment = f'''GN: [{', '.join(target_names)}]'''
+      module.proto = {'type': 'lite', 'canonical_path_from_root': False}
+      module.srcs = module_sources
+      blueprint.add_module(module)
+    else:
+      raise Error('Unhandled proto group type: {}'.format(group.type))
 
 
 def _get_cflags(target: GnParser.Target):
@@ -1228,8 +1244,8 @@
   # checker = gn_utils.ODRChecker(gn, target_name)
 
   # Add any proto groups to the blueprint.
-  for l_name, t_names in proto_groups.items():
-    create_proto_group_modules(blueprint, gn, l_name, t_names)
+  for name, group in proto_groups.items():
+    create_proto_group_modules(blueprint, gn, name, group)
 
   output = [
       """// Copyright (C) 2017 The Android Open Source Project
diff --git a/tools/gen_tp_table_headers.py b/tools/gen_tp_table_headers.py
index f15245e..91e4cdb 100755
--- a/tools/gen_tp_table_headers.py
+++ b/tools/gen_tp_table_headers.py
@@ -72,7 +72,8 @@
   ]
   headers: Dict[str, Header] = {}
   for table in parse_tables_from_modules(modules):
-    input_path = os.path.relpath(table.table.python_module, ROOT_DIR)
+    raw_path = table.table.python_module
+    input_path = raw_path[raw_path.rfind('/src') + 1:]
     header = headers.get(input_path, Header([]))
     header.tables.append(table)
     headers[input_path] = header
diff --git a/tools/gn_utils.py b/tools/gn_utils.py
index d0417d7..904760e 100644
--- a/tools/gn_utils.py
+++ b/tools/gn_utils.py
@@ -529,9 +529,8 @@
     return metadata.get('exports', [])
 
   def get_proto_paths(self, proto_desc):
-    # import_dirs in metadata will be available for source_set targets.
     metadata = proto_desc.get('metadata', {})
-    return metadata.get('import_dirs', [])
+    return metadata.get('proto_import_dirs', [])
 
   def get_proto_target_type(self, target: Target
                            ) -> Tuple[Optional[str], Optional[Dict]]:
diff --git a/tools/heap_profile b/tools/heap_profile
index 700cfeb..eec9508 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -216,6 +216,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -232,28 +235,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/tools/record_android_trace b/tools/record_android_trace
index ceb0808..f2a48c9 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -201,6 +201,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -217,28 +220,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/tools/trace_processor b/tools/trace_processor
index 0917f54..972c9af 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -212,6 +212,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -228,28 +231,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/tools/tracebox b/tools/tracebox
index a9a89e6..144df3a 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -198,6 +198,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -214,28 +217,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/tools/traceconv b/tools/traceconv
index 8ba7114..817022c 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -212,6 +212,9 @@
 import platform
 import subprocess
 import sys
+import threading
+
+DOWNLOAD_LOCK = threading.Lock()
 
 
 def download_or_get_cached(file_name, url, sha256):
@@ -228,28 +231,36 @@
   sha256_path = os.path.join(dir, file_name + '.sha256')
   needs_download = True
 
-  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
-  # download is cached into file_name.sha256, just check if that matches.
-  if os.path.exists(bin_path) and os.path.exists(sha256_path):
-    with open(sha256_path, 'rb') as f:
-      digest = f.read().decode()
-      if digest == sha256:
-        needs_download = False
+  try:
+    # In BatchTraceProcessor, many threads can be trying to execute the below
+    # code in parallel. For this reason, protect the whole operation with a
+    # lock.
+    DOWNLOAD_LOCK.acquire()
 
-  if needs_download:
-    # Either the filed doesn't exist or the SHA256 doesn't match.
-    tmp_path = bin_path + '.tmp'
-    print('Downloading ' + url)
-    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
-    with open(tmp_path, 'rb') as fd:
-      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
-    if actual_sha256 != sha256:
-      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
-                      (url, actual_sha256, sha256))
-    os.chmod(tmp_path, 0o755)
-    os.replace(tmp_path, bin_path)
-    with open(sha256_path, 'w') as f:
-      f.write(sha256)
+    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
+    # download is cached into file_name.sha256, just check if that matches.
+    if os.path.exists(bin_path) and os.path.exists(sha256_path):
+      with open(sha256_path, 'rb') as f:
+        digest = f.read().decode()
+        if digest == sha256:
+          needs_download = False
+
+    if needs_download:
+      # Either the filed doesn't exist or the SHA256 doesn't match.
+      tmp_path = bin_path + '.tmp'
+      print('Downloading ' + url)
+      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
+      with open(tmp_path, 'rb') as fd:
+        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
+      if actual_sha256 != sha256:
+        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
+                        (url, actual_sha256, sha256))
+      os.chmod(tmp_path, 0o755)
+      os.replace(tmp_path, bin_path)
+      with open(sha256_path, 'w') as f:
+        f.write(sha256)
+  finally:
+    DOWNLOAD_LOCK.release()
   return bin_path
 
 
diff --git a/ui/OWNERS b/ui/OWNERS
index aaf587f..d6b4b3d 100644
--- a/ui/OWNERS
+++ b/ui/OWNERS
@@ -1,4 +1,3 @@
-hjd@google.com
 primiano@google.com
 stevegolton@google.com
 
diff --git a/ui/package.json b/ui/package.json
index e1807ce..9dea540 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -72,5 +72,8 @@
     "build": "node build.js",
     "test": "node build.js --run-unittests",
     "lint": "npx eslint . --ext .js,.ts"
+  },
+  "jest": {
+    "setupFiles": ["jest-localstorage-mock"]
   }
 }
diff --git a/ui/release/OWNERS b/ui/release/OWNERS
index 418a82a..4108764 100644
--- a/ui/release/OWNERS
+++ b/ui/release/OWNERS
@@ -1,6 +1,5 @@
 set noparent
 eseckler@google.com
-hjd@google.com
 lalitm@google.com
 primiano@google.com
 skyostil@google.com
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 5f4dd4d..c87709e 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
     },
     {
       "name": "canary",
-      "rev": "58439240cbad83689a1fbafd8234a379a56be7c1"
+      "rev": "9945c86178de704ff44bef7f64c020ff79aedd28"
     },
     {
       "name": "autopush",
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index d09690a..ff66708 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -446,112 +446,46 @@
   header.stale {
     color: grey;
   }
+}
 
-  .rows {
-    position: relative;
-    direction: ltr;
-    width: 100%;
+.pf-ftrace-explorer {
+  height: 100%;
+  font-size: 11px;
+  font-family: var(--monospace-font);
 
-    .row {
-      @include transition();
-      position: absolute;
-      width: 100%;
-      height: 20px;
-      line-height: 20px;
-      background-color: hsl(214, 22%, 100%);
+  .colour {
+    display: inline-block;
+    height: 10px;
+    width: 10px;
+    margin-right: 4px;
+  }
+}
 
-      &.D {
-        color: hsl(122, 20%, 40%);
-      }
-      &.V {
-        color: hsl(122, 20%, 30%);
-      }
-      &.I {
-        color: hsl(0, 0%, 20%);
-      }
-      &.W {
-        color: hsl(45, 60%, 45%);
-      }
-      &.E {
-        color: hsl(4, 90%, 58%);
-      }
-      &.F {
-        color: hsl(291, 64%, 42%);
-      }
-      &.stale {
-        color: #aaa;
-      }
-      &:nth-child(even) {
-        background-color: hsl(214, 22%, 95%);
-      }
-      &:hover {
-        background-color: $table-hover-color;
-      }
-      .cell {
-        font-size: 11px;
-        font-family: var(--monospace-font);
-        white-space: nowrap;
-        overflow: scroll;
-        padding-left: 10px;
-        padding-right: 10px;
-        display: inline-block;
-        &:first-child {
-          padding-left: 5px;
-        }
-        &:last-child {
-          padding-right: 5px;
-        }
-        &:only-child {
-          width: 100%;
-        }
+.pf-android-logs-table {
+  height: 100%;
+  font-size: 11px;
+  font-family: var(--monospace-font);
 
-        // The following children will be used as columns in the table showing
-        // Android logs.
-
-        // 1.Timestamp
-        &:nth-child(1) {
-          width: 7rem;
-          text-overflow: clip;
-          text-align: right;
-        }
-        // 2.Level
-        &:nth-child(2) {
-          width: 4rem;
-        }
-        // 3.Tag
-        &:nth-child(3) {
-          width: 13rem;
-        }
-
-        &.with-process {
-          // 4.Process name
-          &:nth-child(4) {
-            width: 18rem;
-          }
-          // 5.Message - a long string, will take most of the display space.
-          &:nth-child(5) {
-            width: calc(100% - 42rem);
-          }
-        }
-
-        &.no-process {
-          // 4.Message - a long string, will take most of the display space.
-          &:nth-child(4) {
-            width: calc(100% - 24rem);
-          }
-        }
-
-        &.row-header {
-          text-align: left;
-          font-weight: bold;
-          font-size: 13px;
-        }
-
-        &.row-header:first-child {
-          padding-left: 15px;
-        }
-      }
-    }
+  .D {
+    color: hsl(122, 20%, 40%);
+  }
+  .V {
+    color: hsl(122, 20%, 30%);
+  }
+  .I {
+    color: hsl(0, 0%, 20%);
+  }
+  .W {
+    color: hsl(45, 60%, 45%);
+  }
+  .E {
+    color: hsl(4, 90%, 58%);
+  }
+  .F {
+    color: hsl(291, 64%, 42%);
+  }
+  .pf-highlighted {
+    background: #d2efe0;
   }
 }
 
@@ -559,109 +493,6 @@
   margin: 10px;
 }
 
-.ftrace-panel {
-  display: contents;
-
-  .sticky {
-    position: sticky;
-    top: 0;
-    left: 0;
-    z-index: 1;
-    background-color: white;
-    color: #3c4b5d;
-    padding: 5px 10px;
-    display: grid;
-    grid-template-columns: auto auto;
-    justify-content: space-between;
-  }
-
-  .ftrace-rows-label {
-    display: flex;
-    align-items: center;
-  }
-
-  header.stale {
-    color: grey;
-  }
-
-  .rows {
-    position: relative;
-    direction: ltr;
-    min-width: 100%;
-    font-size: 12px;
-
-    .row {
-      @include transition();
-      position: absolute;
-      min-width: 100%;
-      line-height: 20px;
-      background-color: hsl(214, 22%, 100%);
-      white-space: nowrap;
-
-      &:nth-child(even) {
-        background-color: hsl(214, 22%, 95%);
-      }
-
-      &:hover {
-        background-color: $table-hover-color;
-      }
-
-      .cell {
-        font-family: var(--monospace-font);
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        margin-right: 8px;
-        display: inline-block;
-
-        .colour {
-          display: inline-block;
-          height: 10px;
-          width: 10px;
-          margin-right: 4px;
-        }
-
-        &:first-child {
-          margin-left: 8px;
-        }
-
-        &:last-child {
-          margin-right: 8px;
-        }
-
-        &:only-child {
-          width: 100%;
-        }
-
-        // Timestamp
-        &:nth-child(1) {
-          width: 13em;
-          // text-align: right;
-        }
-
-        // Name
-        &:nth-child(2) {
-          width: 24em;
-        }
-
-        // CPU
-        &:nth-child(3) {
-          width: 3em;
-        }
-
-        // Process
-        &:nth-child(4) {
-          width: 24em;
-        }
-
-        &.row-header {
-          font-weight: bold;
-        }
-      }
-    }
-  }
-}
-
 .screenshot-panel {
   height: 100%;
   img {
diff --git a/ui/src/assets/panel_container.scss b/ui/src/assets/panel_container.scss
index 2356ff8..32a4ce5 100644
--- a/ui/src/assets/panel_container.scss
+++ b/ui/src/assets/panel_container.scss
@@ -33,6 +33,7 @@
       &.pf-sticky {
         position: sticky;
         top: 0;
+        z-index: 1;
       }
     }
   }
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 8bb1cdc..567deae 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -56,3 +56,4 @@
 @import "widgets/hotkey";
 @import "widgets/text_paragraph";
 @import "widgets/treetable";
+@import "widgets/virtual_table";
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
index c090dc9..690231e 100644
--- a/ui/src/assets/viewer_page.scss
+++ b/ui/src/assets/viewer_page.scss
@@ -42,7 +42,7 @@
     overflow: auto;
   }
 
-  .header {
+  .pf-timeline-header {
     display: flex;
     flex-direction: row;
     box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
diff --git a/ui/src/assets/widgets/button.scss b/ui/src/assets/widgets/button.scss
index a5e8f9a..65f6a56 100644
--- a/ui/src/assets/widgets/button.scss
+++ b/ui/src/assets/widgets/button.scss
@@ -18,8 +18,6 @@
   font-family: $pf-font;
   line-height: 1;
   user-select: none;
-  color: $pf-primary-foreground;
-  background: $pf-primary-background;
   transition: background $pf-anim-timing, box-shadow $pf-anim-timing;
   border-radius: $pf-border-radius;
   padding: 4px 8px;
@@ -42,45 +40,47 @@
     line-height: inherit;
   }
 
-  &:hover {
-    background: $pf-primary-background-hover;
-  }
-
-  &:active,
-  &.pf-active {
-    transition: none;
-    background: $pf-primary-background-active;
-    box-shadow: inset 1px 1px 4px #00000040;
-  }
-
   &:focus-visible {
     @include focus;
   }
 
+  background: $pf-minimal-background;
+  color: inherit;
+
+  &:hover {
+    background: $pf-minimal-background-hover;
+  }
+
+  &:active,
+  &.pf-active {
+    background: $pf-minimal-background-active;
+  }
+
   &[disabled] {
-    background: $pf-primary-background-disabled;
-    color: $pf-primary-foreground-disabled;
-    box-shadow: none;
+    color: $pf-minimal-foreground-disabled;
+    background: $pf-minimal-background-disabled;
     cursor: not-allowed;
   }
 
   // Remove default background in minimal mode, showing only the text
-  &.pf-minimal {
-    background: $pf-minimal-background;
-    color: inherit;
+  &.pf-intent-primary {
+    color: $pf-primary-foreground;
+    background: $pf-primary-background;
 
     &:hover {
-      background: $pf-minimal-background-hover;
+      background: $pf-primary-background-hover;
     }
 
     &:active,
     &.pf-active {
-      background: $pf-minimal-background-active;
+      transition: none;
+      background: $pf-primary-background-active;
+      box-shadow: inset 1px 1px 4px #00000040;
     }
-
     &[disabled] {
-      color: $pf-minimal-foreground-disabled;
-      background: $pf-minimal-background-disabled;
+      background: $pf-primary-background-disabled;
+      color: $pf-primary-foreground-disabled;
+      box-shadow: none;
       cursor: not-allowed;
     }
   }
diff --git a/ui/src/assets/widgets/details_shell.scss b/ui/src/assets/widgets/details_shell.scss
index a71f237..4888f6b 100644
--- a/ui/src/assets/widgets/details_shell.scss
+++ b/ui/src/assets/widgets/details_shell.scss
@@ -63,6 +63,8 @@
 
   &.pf-fill-parent {
     height: 100%;
+    overflow-y: hidden;
+
     .pf-content {
       overflow-y: auto;
     }
diff --git a/ui/src/assets/widgets/virtual_table.scss b/ui/src/assets/widgets/virtual_table.scss
new file mode 100644
index 0000000..acd22d5
--- /dev/null
+++ b/ui/src/assets/widgets/virtual_table.scss
@@ -0,0 +1,89 @@
+// 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.
+
+@use "sass:math";
+@import "theme";
+
+// Adding these to a new layer makes other rules take precedence
+@layer widgets {
+  .pf-vtable {
+    overflow: auto;
+    font-family: $pf-font;
+    position: relative;
+    background: white; // Performance tweak - see b/335451611
+
+    .pf-vtable-content {
+      display: inline-flex;
+      flex-direction: column;
+      min-width: 100%;
+
+      .pf-vtable-header {
+        font-weight: bold;
+        position: sticky;
+        top: 0;
+        z-index: 1;
+        background: white;
+        white-space: nowrap;
+        padding-inline: 4px;
+
+        // A shadow improves distinction between header and content
+        box-shadow: #0001 0px 0px 8px;
+      }
+
+      .pf-vtable-data {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        margin-right: 8px;
+        display: inline-block;
+      }
+
+      .pf-vtable-slider {
+        overflow: hidden;
+
+        // Necessary trig because we have a 45deg stripes
+        $pattern-density: 1px * math.sqrt(2);
+        $pattern-col: #ddd;
+        overflow: hidden;
+
+        background: repeating-linear-gradient(
+          -45deg,
+          $pattern-col,
+          $pattern-col $pattern-density,
+          white $pattern-density,
+          white $pattern-density * 2
+        );
+
+        .pf-vtable-puck {
+          .pf-vtable-row {
+            white-space: nowrap;
+            padding-inline: 4px;
+
+            &:nth-child(odd) {
+              background-color: hsl(214, 22%, 95%);
+            }
+
+            &:nth-child(even) {
+              background-color: white;
+            }
+
+            &:hover {
+              background-color: $table-hover-color;
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/ui/src/base/geom.ts b/ui/src/base/geom.ts
index f569400..5a20023 100644
--- a/ui/src/base/geom.ts
+++ b/ui/src/base/geom.ts
@@ -24,6 +24,11 @@
   readonly height: number;
 }
 
+export interface Vector {
+  readonly x: number;
+  readonly y: number;
+}
+
 export function intersectRects(a: Rect, b: Rect): Rect {
   return {
     top: Math.max(a.top, b.top),
@@ -57,3 +62,28 @@
     height: r.bottom - r.top,
   };
 }
+
+/**
+ * Return true if rect a contains rect b.
+ *
+ * @param a A rect.
+ * @param b Another rect.
+ * @returns True if rect a contains rect b, false otherwise.
+ */
+export function containsRect(a: Rect, b: Rect): boolean {
+  return !(
+    b.top < a.top ||
+    b.bottom > a.bottom ||
+    b.left < a.left ||
+    b.right > a.right
+  );
+}
+
+export function translateRect(a: Rect, b: Vector): Rect {
+  return {
+    top: a.top + b.y,
+    left: a.left + b.x,
+    bottom: a.bottom + b.y,
+    right: a.right + b.x,
+  };
+}
diff --git a/ui/src/base/logging.ts b/ui/src/base/logging.ts
index 74cbe44..04e0bc7 100644
--- a/ui/src/base/logging.ts
+++ b/ui/src/base/logging.ts
@@ -134,6 +134,6 @@
 // 2) A compile time check where typescript asserts that the value passed can be
 // cast to the "never" type.
 // This is useful for ensuring we exhastively check union types.
-export function assertUnreachable(value: never) {
+export function assertUnreachable(value: never): never {
   throw new Error(`This code should not be reachable ${value as unknown}`);
 }
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4910cbd..92cad97 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -1162,13 +1162,6 @@
       );
   },
 
-  setPivotTableArgumentNames(
-    state: StateDraft,
-    args: {argumentNames: string[]},
-  ) {
-    state.nonSerializableState.pivotTable.argumentNames = args.argumentNames;
-  },
-
   changePivotTablePivotOrder(
     state: StateDraft,
     args: {from: number; to: number; direction: DropDirection},
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 5bf1f5a..f866914 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -83,7 +83,6 @@
       ],
       constrainToArea: true,
       queryRequested: false,
-      argumentNames: [],
     },
   };
 }
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index 0817ebf..acf2ee8 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -24,7 +24,7 @@
   id: 'showHeapGraphDominatorTree',
   name: 'Show heap graph dominator tree',
   description: 'Show dominated size and objects tabs in Java heap graph view.',
-  defaultValue: false,
+  defaultValue: true,
 });
 
 export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
index 334ad25..133d606 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/common/recordingV2/recording_config_utils.ts
@@ -769,7 +769,7 @@
   ) {
     const ds = new TraceConfig.DataSource();
     ds.config = new DataSourceConfig();
-    ds.config.name = 'windows.etw';
+    ds.config.name = 'org.chromium.etw_system';
     ds.config.etwConfig = new EtwConfig();
 
     const kernelFlags: EtwConfig.KernelFlag[] = [];
diff --git a/ui/src/common/search_data.ts b/ui/src/common/search_data.ts
index b81dfb2..7209c04 100644
--- a/ui/src/common/search_data.ts
+++ b/ui/src/common/search_data.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+export type SearchSource = 'cpu' | 'log' | 'slice' | 'track';
+
 export interface SearchSummary {
   tsStarts: BigInt64Array;
   tsEnds: BigInt64Array;
@@ -19,10 +21,10 @@
 }
 
 export interface CurrentSearchResults {
-  sliceIds: Float64Array;
-  tsStarts: BigInt64Array;
+  eventIds: Float64Array;
+  tses: BigInt64Array;
   utids: Float64Array;
   trackKeys: string[];
-  sources: string[];
+  sources: SearchSource[];
   totalResults: number;
 }
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 3b27e16..d0af73b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -424,9 +424,6 @@
   // Set to true by frontend to request controller to perform the query to
   // acquire the necessary data from the engine.
   queryRequested: boolean;
-
-  // Argument names in the current trace, used for autocompletion purposes.
-  argumentNames: string[];
 }
 
 export interface LoadedConfigNone {
diff --git a/ui/src/common/thread_state.ts b/ui/src/common/thread_state.ts
index 6ef6188..d94625c 100644
--- a/ui/src/common/thread_state.ts
+++ b/ui/src/common/thread_state.ts
@@ -34,9 +34,18 @@
   ioWait: boolean | undefined = undefined,
 ) {
   if (state === undefined) return '';
-  if (state === 'Running') {
-    return state;
+
+  // Self describing states
+  switch (state) {
+    case 'Running':
+    case 'Initialized':
+    case 'Deferred Ready':
+    case 'Transition':
+    case 'Stand By':
+    case 'Waiting':
+      return state;
   }
+
   if (state === null) {
     return 'Unknown';
   }
diff --git a/ui/src/common/track_cache.ts b/ui/src/common/track_cache.ts
index 3b2eb41..5d05a13 100644
--- a/ui/src/common/track_cache.ts
+++ b/ui/src/common/track_cache.ts
@@ -180,6 +180,7 @@
 }
 
 enum TrackFSMState {
+  NotCreated = 'not_created',
   Creating = 'creating',
   Ready = 'ready',
   UpdatePending = 'update_pending',
@@ -200,20 +201,22 @@
   constructor(
     public track: Track,
     public desc: TrackDescriptor,
-    ctx: TrackContext,
+    private readonly ctx: TrackContext,
   ) {
-    this.state = TrackFSMState.Creating;
-    const result = this.track.onCreate?.(ctx);
-    Promise.resolve(result)
-      .then(() => this.onTrackCreated())
-      .catch((e) => {
-        this.error = e;
-        this.state = TrackFSMState.Error;
-      });
+    this.state = TrackFSMState.NotCreated;
   }
 
   update(): void {
     switch (this.state) {
+      case TrackFSMState.NotCreated:
+        Promise.resolve(this.track.onCreate?.(this.ctx))
+          .then(() => this.onTrackCreated())
+          .catch((e) => {
+            this.error = e;
+            this.state = TrackFSMState.Error;
+          });
+        this.state = TrackFSMState.Creating;
+        break;
       case TrackFSMState.Creating:
       case TrackFSMState.Updating:
         this.state = TrackFSMState.UpdatePending;
@@ -240,6 +243,10 @@
 
   destroy(): void {
     switch (this.state) {
+      case TrackFSMState.NotCreated:
+        // Nothing to do
+        this.state = TrackFSMState.Destroyed;
+        break;
       case TrackFSMState.Ready:
         // Don't bother awaiting this as the track can no longer be used.
         Promise.resolve(this.track.onDestroy?.()).catch(() => {
@@ -267,6 +274,7 @@
         this.track.onDestroy?.();
         this.state = TrackFSMState.Destroyed;
         break;
+      case TrackFSMState.Creating:
       case TrackFSMState.UpdatePending:
         const result = this.track.onUpdate?.();
         Promise.resolve(result)
@@ -277,9 +285,6 @@
           });
         this.state = TrackFSMState.Updating;
         break;
-      case TrackFSMState.Creating:
-        this.state = TrackFSMState.Ready;
-        break;
       case TrackFSMState.Error:
         break;
       default:
diff --git a/ui/src/common/track_cache_unittest.ts b/ui/src/common/track_cache_unittest.ts
index 2be2d55..599a748 100644
--- a/ui/src/common/track_cache_unittest.ts
+++ b/ui/src/common/track_cache_unittest.ts
@@ -63,8 +63,10 @@
   it('reuses tracks', async () => {
     const first = trackCache.resolveTrack('foo', td);
     trackCache.flushOldTracks();
+    first.update();
     const second = trackCache.resolveTrack('foo', td);
     trackCache.flushOldTracks();
+    second.update();
 
     // Ensure onCreate only called once
     expect(track.onCreate).toHaveBeenCalledTimes(1);
@@ -72,7 +74,8 @@
   });
 
   it('destroys tracks', async () => {
-    trackCache.resolveTrack('foo', td);
+    const t = trackCache.resolveTrack('foo', td);
+    t.update();
 
     // Double flush should destroy all tracks
     trackCache.flushOldTracks();
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index 704add7..20afd9e 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -39,33 +39,72 @@
     const duration = area.end - area.start;
     const durationSec = Duration.toSeconds(duration);
 
-    const query = `create view ${this.kind} as select
-    name,
-    count(1) as count,
-    round(sum(weighted_value)/${duration}, 2) as avg_value,
-    last as last_value,
-    first as first_value,
-    max(last) - min(first) as delta_value,
-    round((max(last) - min(first))/${durationSec}, 2) as rate,
-    min(value) as min_value,
-    max(value) as max_value
-    from
-        (select *,
-        (min(ts + dur, ${area.end}) - max(ts,${area.start}))
-        * value as weighted_value,
-        first_value(value) over
-        (partition by track_id order by ts) as first,
-        last_value(value) over
-        (partition by track_id order by ts
-            range between unbounded preceding and unbounded following) as last
-        from experimental_counter_dur
-        where track_id in (${trackIds})
-        and ts + dur >= ${area.start} and
-        ts <= ${area.end})
-    join counter_track
-    on track_id = counter_track.id
-    group by track_id`;
-
+    // TODO(lalitm): Rewrite this query in a way that is both simpler and faster
+    let query;
+    if (trackIds.length === 1) {
+      // Optimized query for the special case where there is only 1 track id.
+      query = `CREATE VIEW ${this.kind} AS
+      WITH aggregated AS (
+        SELECT
+          COUNT(1) AS count,
+          ROUND(SUM(
+            (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
+            2
+          ) AS avg_value,
+          (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
+            AND ts + dur >= ${area.start}
+            AND ts <= ${area.end} ORDER BY ts DESC LIMIT 1)
+            AS last_value,
+          (SELECT value FROM experimental_counter_dur WHERE track_id = ${trackIds[0]}
+            AND ts + dur >= ${area.start}
+            AND ts <= ${area.end} ORDER BY ts ASC LIMIT 1)
+            AS first_value,
+          MIN(value) AS min_value,
+          MAX(value) AS max_value
+        FROM experimental_counter_dur
+          WHERE track_id = ${trackIds[0]}
+          AND ts + dur >= ${area.start}
+          AND ts <= ${area.end})
+      SELECT
+        (SELECT name FROM counter_track WHERE id = ${trackIds[0]}) AS name,
+        *,
+        MAX(last_value) - MIN(first_value) AS delta_value,
+        ROUND((MAX(last_value) - MIN(first_value))/${durationSec}, 2) AS rate
+      FROM aggregated`;
+    } else {
+      // Slower, but general purspose query that can aggregate multiple tracks
+      query = `CREATE VIEW ${this.kind} AS
+      WITH aggregated AS (
+        SELECT track_id,
+          COUNT(1) AS count,
+          ROUND(SUM(
+            (MIN(ts + dur, ${area.end}) - MAX(ts,${area.start}))*value)/${duration},
+            2
+          ) AS avg_value,
+          value_at_max_ts(-ts, value) AS first,
+          value_at_max_ts(ts, value) AS last,
+          MIN(value) AS min_value,
+          MAX(value) AS max_value
+        FROM experimental_counter_dur
+          WHERE track_id IN (${trackIds})
+          AND ts + dur >= ${area.start} AND
+          ts <= ${area.end}
+        GROUP BY track_id
+      )
+      SELECT
+        name,
+        count,
+        avg_value,
+        last AS last_value,
+        first AS first_value,
+        last - first AS delta_value,
+        ROUND((last - first)/${durationSec}, 2) AS rate,
+        min_value,
+        max_value
+      FROM aggregated JOIN counter_track ON
+        track_id = counter_track.id
+      GROUP BY track_id`;
+    }
     await engine.query(query);
     return true;
   }
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index b1c00a4..22160d6 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, time} from '../base/time';
+import {Duration, time} from '../base/time';
 import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {
@@ -25,8 +25,8 @@
   CallsiteInfo,
   FlamegraphState,
   FlamegraphStateViewingOption,
-  ProfileType,
   isHeapGraphDominatorTreeViewingOption,
+  ProfileType,
 } from '../common/state';
 import {FlamegraphDetails, globals} from '../frontend/globals';
 import {publishFlamegraphDetails} from '../frontend/publish';
@@ -106,6 +106,10 @@
     }
     return tableName;
   }
+
+  hasQuery(query: string): boolean {
+    return this.cache.get(query) !== undefined;
+  }
 }
 
 export class FlamegraphController extends Controller<'main'> {
@@ -116,10 +120,6 @@
   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');
@@ -215,9 +215,8 @@
       const flamegraphData = await this.getFlamegraphData(
         key,
         /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        selectedFlamegraphState.viewingOption
-          ? /* eslint-enable */
-            selectedFlamegraphState.viewingOption
+        selectedFlamegraphState.viewingOption /* eslint-enable */
+          ? selectedFlamegraphState.viewingOption
           : defaultViewingOption(selectedFlamegraphState.type),
         selection.start,
         selection.end,
@@ -540,22 +539,18 @@
   }
 
   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;
+    const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`;
+    const outputQuery = `SELECT * FROM ${outputTableName}`;
+    if (this.cache.hasQuery(outputQuery)) {
+      return outputQuery;
     }
-    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
+    CREATE PERFETTO TABLE _heap_graph_object_dominated AS
     SELECT
      node.id,
      node.idom_id,
@@ -574,21 +569,19 @@
     -- 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 (
+    WITH RECURSIVE _tree_visitor(id, path_hash) AS (
       SELECT
         id,
-        CAST(type_id AS text) || '-' || IFNULL(root_type, '') AS path,
         HASH(
-          CAST(type_id AS text) || '-' || IFNULL(root_type, '')
+          CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '')
         ) AS path_hash
-      FROM heap_graph_object_dominated
+      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
+        HASH(CAST(parent.path_hash AS TEXT) || '/' || 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
@@ -597,7 +590,7 @@
     -- 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
+    CREATE PERFETTO TABLE ${outputTableName} AS
     SELECT
       map.path_hash as id,
       COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
@@ -615,13 +608,18 @@
       -1 as line_number,
       sum(self_size) AS size,
       count(*) AS count
-    FROM heap_graph_object_dominated node
+    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;`);
+    GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;
 
-    return selectTreeQuery;
+    -- These are intermediates and not needed
+    DROP TABLE _heap_graph_object_dominated;
+    DROP TABLE _dominator_tree_path_hash;
+    `);
+
+    return outputQuery;
   }
 
   getMinSizeDisplayed(
diff --git a/ui/src/controller/pivot_table_controller.ts b/ui/src/controller/pivot_table_controller.ts
index b1eb386..663fdf4 100644
--- a/ui/src/controller/pivot_table_controller.ts
+++ b/ui/src/controller/pivot_table_controller.ts
@@ -31,7 +31,7 @@
 } from '../frontend/pivot_table_query_generator';
 import {Aggregation, PivotTree} from '../frontend/pivot_table_types';
 import {Engine} from '../trace_processor/engine';
-import {ColumnType, STR} from '../trace_processor/query_result';
+import {ColumnType} from '../trace_processor/query_result';
 
 import {Controller} from './controller';
 
@@ -189,7 +189,6 @@
   engine: Engine;
   lastQueryAreaId = '';
   lastQueryAreaTracks = new Set<string>();
-  requestedArgumentNames = false;
 
   constructor(args: {engine: Engine}) {
     super({});
@@ -272,31 +271,11 @@
     );
   }
 
-  async requestArgumentNames() {
-    this.requestedArgumentNames = true;
-    const result = await this.engine.query(`
-      select distinct flat_key from args
-    `);
-    const it = result.iter({flat_key: STR});
-
-    const argumentNames = [];
-    while (it.valid()) {
-      argumentNames.push(it.flat_key);
-      it.next();
-    }
-
-    globals.dispatch(Actions.setPivotTableArgumentNames({argumentNames}));
-  }
-
   run() {
     if (!PIVOT_TABLE_REDUX_FLAG.get()) {
       return;
     }
 
-    if (!this.requestedArgumentNames) {
-      this.requestArgumentNames();
-    }
-
     const pivotTableState = globals.state.nonSerializableState.pivotTable;
     const selection = getLegacySelection(globals.state);
 
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 5caff6e..61ada93 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -15,7 +15,11 @@
 import {sqliteString} from '../base/string_utils';
 import {Duration, duration, Span, time, Time, TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
-import {CurrentSearchResults, SearchSummary} from '../common/search_data';
+import {
+  CurrentSearchResults,
+  SearchSource,
+  SearchSummary,
+} from '../common/search_data';
 import {OmniboxState} from '../common/state';
 import {globals} from '../frontend/globals';
 import {publishSearch, publishSearchResult} from '../frontend/publish';
@@ -100,8 +104,8 @@
         count: new Uint8Array(0),
       });
       publishSearchResult({
-        sliceIds: new Float64Array(0),
-        tsStarts: new BigInt64Array(0),
+        eventIds: new Float64Array(0),
+        tses: new BigInt64Array(0),
         utids: new Float64Array(0),
         sources: [],
         trackKeys: [],
@@ -203,11 +207,9 @@
     // easier once the track table has entries for all the tracks.
     const cpuToTrackId = new Map();
     for (const track of Object.values(globals.state.tracks)) {
-      if (exists(track?.uri)) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key);
-        }
+      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
+      if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key);
       }
     }
 
@@ -220,60 +222,83 @@
       utids.push(it.utid);
     }
 
-    const queryRes = await this.query(`
-    select
-      id as sliceId,
-      ts,
-      'cpu' as source,
-      cpu as sourceId,
-      utid
-    from sched where utid in (${utids.join(',')})
-    union
-    select
-      slice_id as sliceId,
-      ts,
-      'track' as source,
-      track_id as sourceId,
-      0 as utid
-      from slice
-      where slice.name glob ${searchLiteral}
-        or (
-          0 != CAST(${sqliteString(search)} AS INT) and
-          sliceId = CAST(${sqliteString(search)} AS INT)
-        )
-    union
-    select
-      slice_id as sliceId,
-      ts,
-      'track' as source,
-      track_id as sourceId,
-      0 as utid
-      from slice
-      join args using(arg_set_id)
-      where string_value glob ${searchLiteral} or key glob ${searchLiteral}
-    union
-    select
-      id as sliceId,
-      ts,
-      'log' as source,
-      0 as sourceId,
-      utid
-    from android_logs where msg glob ${searchLiteral}
-    order by ts
-
+    const res = await this.query(`
+      select
+        id as sliceId,
+        ts,
+        'cpu' as source,
+        cpu as sourceId,
+        utid
+      from sched where utid in (${utids.join(',')})
+      union all
+      select *
+      from (
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+          from slice
+          where slice.name glob ${searchLiteral}
+            or (
+              0 != CAST(${sqliteString(search)} AS INT) and
+              sliceId = CAST(${sqliteString(search)} AS INT)
+            )
+        union
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+        from slice
+        join args using(arg_set_id)
+        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
+      )
+      union all
+      select
+        id as sliceId,
+        ts,
+        'log' as source,
+        0 as sourceId,
+        utid
+      from android_logs where msg glob ${searchLiteral}
+      order by ts
     `);
 
-    const rows = queryRes.numRows();
     const searchResults: CurrentSearchResults = {
-      sliceIds: new Float64Array(rows),
-      tsStarts: new BigInt64Array(rows),
-      utids: new Float64Array(rows),
-      trackKeys: [],
+      eventIds: new Float64Array(0),
+      tses: new BigInt64Array(0),
+      utids: new Float64Array(0),
       sources: [],
+      trackKeys: [],
       totalResults: 0,
     };
 
-    const it = queryRes.iter({
+    const lowerSearch = search.toLowerCase();
+    for (const track of Object.values(globals.state.tracks)) {
+      if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
+        continue;
+      }
+      searchResults.totalResults++;
+      searchResults.sources.push('track');
+      searchResults.trackKeys.push(track.key);
+    }
+
+    const rows = res.numRows();
+    searchResults.eventIds = new Float64Array(
+      searchResults.totalResults + rows,
+    );
+    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
+    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
+    for (let i = 0; i < searchResults.totalResults; ++i) {
+      searchResults.eventIds[i] = -1;
+      searchResults.tses[i] = -1n;
+      searchResults.utids[i] = -1;
+    }
+
+    const it = res.iter({
       sliceId: NUM,
       ts: LONG,
       source: STR,
@@ -284,7 +309,7 @@
       let trackId = undefined;
       if (it.source === 'cpu') {
         trackId = cpuToTrackId.get(it.sourceId);
-      } else if (it.source === 'track') {
+      } else if (it.source === 'slice') {
         trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
       } else if (it.source === 'log') {
         const logTracks = Object.values(globals.state.tracks).filter(
@@ -305,9 +330,9 @@
 
       const i = searchResults.totalResults++;
       searchResults.trackKeys.push(trackId);
-      searchResults.sources.push(it.source);
-      searchResults.sliceIds[i] = it.sliceId;
-      searchResults.tsStarts[i] = it.ts;
+      searchResults.sources.push(it.source as SearchSource);
+      searchResults.eventIds[i] = it.sliceId;
+      searchResults.tses[i] = it.ts;
       searchResults.utids[i] = it.utid;
     }
     return searchResults;
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 5194215..c1f0d6e 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -201,20 +201,14 @@
 // ensure it's only run once.
 async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> {
   await engine.query(`
-    create or replace perfetto table __max_layout_depth_state as
-    select track_id, max(depth) as max_depth
-    from slice
-    group by track_id
-    order by track_id;
-
     create perfetto function __max_layout_depth(track_count INT, track_ids STRING)
     returns INT AS
     select iif(
       $track_count = 1,
       (
         select max_depth
-        from __max_layout_depth_state
-        where track_id = cast($track_ids AS int)
+        from _slice_track_summary
+        where id = cast($track_ids AS int)
       ),
       (
         select max(layout_depth)
@@ -517,6 +511,7 @@
 
     // Make sure the helper views are available before we start adding tracks.
     await this.initialiseHelperViews();
+    await this.includeSummaryTables();
 
     await defineMaxLayoutDepthSqlFunction(engine);
 
@@ -1097,6 +1092,22 @@
     }
   }
 
+  async includeSummaryTables() {
+    const engine = assertExists<Engine>(this.engine);
+
+    this.updateStatus('Creating slice summaries');
+    await engine.query(`include perfetto module viz.summary.slices;`);
+
+    this.updateStatus('Creating thread summaries');
+    await engine.query(`include perfetto module viz.summary.threads;`);
+
+    this.updateStatus('Creating processes summaries');
+    await engine.query(`include perfetto module viz.summary.processes;`);
+
+    this.updateStatus('Creating track summaries');
+    await engine.query(`include perfetto module viz.summary.tracks;`);
+  }
+
   private updateStatus(msg: string): void {
     globals.dispatch(
       Actions.updateStatus({
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index d8f42fb..b6dd388 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -167,64 +167,35 @@
 
   async addGlobalAsyncTracks(engine: EngineProxy): Promise<void> {
     const rawGlobalAsyncTracks = await engine.query(`
-      with tracks_with_slices as materialized (
-        select distinct track_id
-        from slice
-      ),
-      global_tracks as (
-        select
-          track.parent_id as parent_id,
-          track.id as track_id,
-          track.name as name
-        from track
-        join tracks_with_slices on tracks_with_slices.track_id = track.id
-        where
-          track.type = "track"
-          or track.type = "gpu_track"
-          or track.type = "cpu_track"
-      ),
-      global_tracks_grouped as (
-        select
-          parent_id,
-          name,
-          group_concat(track_id) as trackIds,
-          count(track_id) as trackCount
-        from global_tracks track
-        group by parent_id, name
+      with global_tracks_grouped as (
+        select distinct t.parent_id, t.name
+        from track t
+        join _slice_track_summary using (id)
+        where t.type in ('track', 'gpu_track', 'cpu_track')
       )
       select
-        t.parent_id as parentId,
-        p.name as parentName,
         t.name as name,
-        t.trackIds as trackIds,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+        t.parent_id as parentId,
+        p.name as parentName
       from global_tracks_grouped AS t
       left join track p on (t.parent_id = p.id)
-      order by p.name, t.name;
+      order by p.name, t.name
     `);
     const it = rawGlobalAsyncTracks.iter({
       name: STR_NULL,
-      parentName: STR_NULL,
       parentId: NUM_NULL,
-      maxDepth: NUM_NULL,
+      parentName: STR_NULL,
     });
 
     const parentIdToGroupId = new Map<number, string>();
-
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
       const rawParentName = it.parentName === null ? undefined : it.parentName;
       const name = getTrackName({name: rawName, kind});
       const parentTrackId = it.parentId;
-      const maxDepth = it.maxDepth;
       let trackGroup = SCROLLING_TRACK_GROUP;
 
-      // If there are no slices in this track, skip it.
-      if (maxDepth === null) {
-        continue;
-      }
-
       if (parentTrackId !== null) {
         const groupId = parentIdToGroupId.get(parentTrackId);
         if (groupId === undefined) {
@@ -261,11 +232,11 @@
       // Only add a gpu freq track if we have
       // gpu freq data.
       const freqExistsResult = await engine.query(`
-      select *
-      from gpu_counter_track
-      where name = 'gpufreq' and gpu_id = ${gpu}
-      limit 1;
-    `);
+        select *
+        from gpu_counter_track
+        where name = 'gpufreq' and gpu_id = ${gpu}
+        limit 1;
+      `);
       if (freqExistsResult.numRows() > 0) {
         this.tracksToAdd.push({
           uri: `perfetto.Counter#gpu_freq${gpu}`,
@@ -638,14 +609,14 @@
 
   async addThreadStateTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with ts_distinct as materialized (select distinct utid from thread_state)
       select
         utid,
         upid,
         tid,
         thread.name as threadName
       from thread
-      where utid != 0 and utid in ts_distinct`);
+      join _sched_summary using (utid)
+    `);
 
     const it = result.iter({
       utid: NUM,
@@ -726,16 +697,16 @@
 
   async addThreadCounterTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-    select
-      thread_counter_track.name as trackName,
-      utid,
-      upid,
-      tid,
-      thread.name as threadName,
-      thread_counter_track.id as trackId
-    from thread_counter_track
-    join thread using(utid)
-    where thread_counter_track.name != 'thread_time'
+      select
+        thread_counter_track.name as trackName,
+        utid,
+        upid,
+        tid,
+        thread.name as threadName,
+        thread_counter_track.id as trackId
+      from thread_counter_track
+      join thread using(utid)
+      where thread_counter_track.name != 'thread_time'
   `);
 
     const it = result.iter({
@@ -776,27 +747,15 @@
 
   async addProcessAsyncSliceTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where
-            process_track.name is null or
-            process_track.name not like "% Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name is null or t.name not glob "* Timeline"
     `);
 
     const it = result.iter({
@@ -805,7 +764,6 @@
       trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
-      maxDepth: NUM_NULL,
     });
     for (; it.valid(); it.next()) {
       const upid = it.upid;
@@ -813,14 +771,8 @@
       const rawTrackIds = it.trackIds;
       const processName = it.processName;
       const pid = it.pid;
-      const maxDepth = it.maxDepth;
 
-      if (maxDepth === null) {
-        // If there are no slices in this track, skip it.
-        continue;
-      }
-
-      const uuid = this.getUuid(0, upid);
+      const uuid = this.getUuid(null, upid);
       const name = getTrackName({
         name: trackName,
         upid,
@@ -840,37 +792,18 @@
 
   async addUserAsyncSliceTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with tracks_with_slices as materialized (
-        select distinct track_id
-        from slice
-      ),
-      global_tracks as (
-        select
-          uid_track.name,
-          uid_track.uid,
-          group_concat(uid_track.id) as trackIds,
-          count(uid_track.id) as trackCount
-        from uid_track
-        join tracks_with_slices
-        where tracks_with_slices.track_id == uid_track.id
-        group by uid_track.uid
-      )
       select
         t.name as name,
         t.uid as uid,
-        package_list.package_name as package_name,
-        t.trackIds as trackIds,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks t
-      join package_list
-      where t.uid = package_list.uid
-      group by t.uid
-      `);
+        package_list.package_name as packageName
+      from _uid_track_track_summary_by_uid_and_name t
+      join package_list using (uid)
+    `);
 
     const it = result.iter({
       name: STR_NULL,
       uid: NUM_NULL,
-      package_name: STR_NULL,
+      packageName: STR_NULL,
     });
 
     // Map From [name] -> [uuid, key]
@@ -882,8 +815,7 @@
       }
       const rawName = it.name;
       const uid = it.uid === null ? undefined : it.uid;
-      const userName =
-        it.package_name === null ? `UID: ${uid}` : it.package_name;
+      const userName = it.packageName === null ? `UID: ${uid}` : it.packageName;
 
       const groupUuid = `uid-track-group${rawName}`;
       if (groupMap.get(rawName) === undefined) {
@@ -910,48 +842,29 @@
 
   async addActualFramesTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where process_track.name = "Actual Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
-  `);
+        upid,
+        t.name as trackName,
+        process.name as processName,
+        process.pid as pid
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name = "Actual Timeline"
+    `);
 
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
       processName: STR_NULL,
       pid: NUM_NULL,
-      maxDepth: NUM_NULL,
     });
     for (; it.valid(); it.next()) {
       const upid = it.upid;
       const trackName = it.trackName;
       const processName = it.processName;
       const pid = it.pid;
-      const maxDepth = it.maxDepth;
 
-      if (maxDepth === null) {
-        // If there are no slices in this track, skip it.
-        continue;
-      }
-
-      const uuid = this.getUuid(0, upid);
-
+      const uuid = this.getUuid(null, upid);
       const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
       const name = getTrackName({
         name: trackName,
@@ -972,33 +885,21 @@
 
   async addExpectedFramesTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where process_track.name = "Expected Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
-  `);
+        upid,
+        t.name as trackName,
+        process.name as processName,
+        process.pid as pid
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name = "Expected Timeline"
+    `);
 
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
       processName: STR_NULL,
       pid: NUM_NULL,
-      maxDepth: NUM_NULL,
     });
 
     for (; it.valid(); it.next()) {
@@ -1006,15 +907,8 @@
       const trackName = it.trackName;
       const processName = it.processName;
       const pid = it.pid;
-      const maxDepth = it.maxDepth;
 
-      if (maxDepth === null) {
-        // If there are no slices in this track, skip it.
-        continue;
-      }
-
-      const uuid = this.getUuid(0, upid);
-
+      const uuid = this.getUuid(null, upid);
       const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
       const name = getTrackName({
         name: trackName,
@@ -1035,7 +929,6 @@
 
   async addThreadSliceTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      with slice_track as materialized (select distinct track_id from slice)
       select
         thread_track.utid as utid,
         thread_track.id as trackId,
@@ -1046,8 +939,8 @@
         thread.name as threadName,
         thread.upid as upid
       from thread_track
+      join _slice_track_summary using (id)
       join thread using(utid)
-      join slice_track on thread_track.id = slice_track.track_id
   `);
 
     const it = result.iter({
@@ -1091,14 +984,14 @@
 
   async addProcessCounterTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-    select
-      process_counter_track.id as trackId,
-      process_counter_track.name as trackName,
-      upid,
-      process.pid,
-      process.name as processName
-    from process_counter_track
-    join process using(upid);
+      select
+        process_counter_track.id as trackId,
+        process_counter_track.name as trackName,
+        upid,
+        process.pid,
+        process.name as processName
+      from process_counter_track
+      join process using(upid);
   `);
     const it = result.iter({
       trackId: NUM,
@@ -1113,7 +1006,7 @@
       const trackId = it.trackId;
       const trackName = it.trackName;
       const processName = it.processName;
-      const uuid = this.getUuid(0, upid);
+      const uuid = this.getUuid(null, upid);
       const name = getTrackName({
         name: trackName,
         upid,
@@ -1135,13 +1028,13 @@
 
   async addProcessHeapProfileTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-    select distinct(upid) from heap_profile_allocation
-    union
-    select distinct(upid) from heap_graph_object
+      select upid
+      from _process_available_info_summary
+      where allocation_count > 0 or graph_object_count > 0
   `);
     for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
       const upid = it.upid;
-      const uuid = this.getUuid(0, upid);
+      const uuid = this.getUuid(null, upid);
       this.tracksToAdd.push({
         uri: `perfetto.HeapProfile#${upid}`,
         trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
@@ -1153,14 +1046,15 @@
 
   async addProcessPerfSamplesTracks(engine: EngineProxy): Promise<void> {
     const result = await engine.query(`
-      select distinct upid, pid
-      from perf_sample join thread using (utid) join process using (upid)
-      where callsite_id is not null
+      select upid, pid
+      from _process_available_info_summary
+      join process using (upid)
+      where perf_sample_count > 0
   `);
     for (const it = result.iter({upid: NUM, pid: NUM}); it.valid(); it.next()) {
       const upid = it.upid;
       const pid = it.pid;
-      const uuid = this.getUuid(0, upid);
+      const uuid = this.getUuid(null, upid);
       this.tracksToAdd.push({
         uri: `perfetto.PerfSamplesProfile#${upid}`,
         trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
@@ -1170,22 +1064,22 @@
     }
   }
 
-  getUuidUnchecked(utid: number, upid: number | null) {
+  getUuidUnchecked(utid: number | null, upid: number | null) {
     return upid === null
-      ? this.utidToUuid.get(utid)
+      ? this.utidToUuid.get(utid!)
       : this.upidToUuid.get(upid);
   }
 
-  getUuid(utid: number, upid: number | null) {
+  getUuid(utid: number | null, upid: number | null) {
     return assertExists(this.getUuidUnchecked(utid, upid));
   }
 
-  getOrCreateUuid(utid: number, upid: number | null) {
+  getOrCreateUuid(utid: number | null, upid: number | null) {
     let uuid = this.getUuidUnchecked(utid, upid);
     if (uuid === undefined) {
       uuid = uuidv4();
       if (upid === null) {
-        this.utidToUuid.set(utid, uuid);
+        this.utidToUuid.set(utid!, uuid);
       } else {
         this.upidToUuid.set(upid, uuid);
       }
@@ -1274,181 +1168,155 @@
     //  thread name
     //  utid
     const result = await engine.query(`
-    with candidateThreadsAndProcesses as materialized (
-      select upid, 0 as utid from process_track
-      union
-      select upid, 0 as utid from process_counter_track
-      union
-      select upid, utid from thread_counter_track join thread using(utid)
-      union
-      select upid, utid from thread_track join thread using(utid)
-      union
-      select upid, utid from (
-        select distinct utid from sched
-      ) join thread using(utid) group by utid
-      union
-      select upid, 0 as utid from (
-        select distinct utid from perf_sample where callsite_id is not null
-      ) join thread using (utid)
-      union
-      select upid, utid from (
-        select distinct utid from cpu_profile_stack_sample
-      ) join thread using(utid)
-      union
-      select upid as upid, 0 as utid from heap_profile_allocation
-      union
-      select upid as upid, 0 as utid from heap_graph_object
-    ),
-    schedSum as materialized (
-      select upid, sum(thread_total_dur) as total_dur
-      from (
-        select utid, sum(dur) as thread_total_dur
-        from sched where dur != -1 and utid != 0
-        group by utid
+      with processGroups as (
+        select
+          upid,
+          process.pid as pid,
+          process.name as processName,
+          sum_running_dur as sumRunningDur,
+          thread_slice_count + process_slice_count as sliceCount,
+          perf_sample_count as perfSampleCount,
+          allocation_count as heapProfileAllocationCount,
+          graph_object_count as heapGraphObjectCount,
+          (
+            select group_concat(string_value)
+            from args
+            where
+              process.arg_set_id is not null and
+              arg_set_id = process.arg_set_id and
+              flat_key = 'chrome.process_label'
+          ) chromeProcessLabels,
+          case process.name
+            when 'Browser' then 3
+            when 'Gpu' then 2
+            when 'Renderer' then 1
+            else 0
+          end as chromeProcessRank
+        from _process_available_info_summary
+        join process using(upid)
+      ),
+      threadGroups as (
+        select
+          utid,
+          tid,
+          thread.name as threadName,
+          sum_running_dur as sumRunningDur,
+          slice_count as sliceCount,
+          perf_sample_count as perfSampleCount
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is null
       )
-      join thread using (utid)
-      group by upid
-    ),
-    sliceSum as materialized (
-      select
-        process.upid as upid,
-        sum(cnt) as sliceCount
-      from (select track_id, count(*) as cnt from slice group by track_id)
-        left join thread_track on track_id = thread_track.id
-        left join thread on thread_track.utid = thread.utid
-        left join process_track on track_id = process_track.id
-        join process on process.upid = thread.upid
-          or process_track.upid = process.upid
-      where process.upid is not null
-      group by process.upid
-    )
-    select
-      the_tracks.upid,
-      the_tracks.utid,
-      total_dur as hasSched,
-      hasHeapProfiles,
-      process.pid as pid,
-      thread.tid as tid,
-      process.name as processName,
-      thread.name as threadName,
-      package_list.debuggable as isDebuggable,
-      ifnull((
-        select group_concat(string_value)
-        from args
-        where
-          process.arg_set_id is not null and
-          arg_set_id = process.arg_set_id and
-          flat_key = 'chrome.process_label'
-      ), '') AS chromeProcessLabels,
-      (case process.name
-         when 'Browser' then 3
-         when 'Gpu' then 2
-         when 'Renderer' then 1
-         else 0
-      end) as chromeProcessRank
-    from candidateThreadsAndProcesses the_tracks
-    left join schedSum using(upid)
-    left join (
-      select
-        distinct(upid) as upid,
-        true as hasHeapProfiles
-      from heap_profile_allocation
-      union
-      select
-        distinct(upid) as upid,
-        true as hasHeapProfiles
-      from heap_graph_object
-    ) using (upid)
-    left join (
-      select
-        thread.upid as upid,
-        sum(cnt) as perfSampleCount
+      select *
       from (
-          select utid, count(*) as cnt
-          from perf_sample where callsite_id is not null
-          group by utid
-      ) join thread using (utid)
-      group by thread.upid
-    ) using (upid)
-    left join sliceSum using (upid)
-    left join thread using(utid)
-    left join process using(upid)
-    left join package_list using(uid)
-    order by
-      chromeProcessRank desc,
-      hasHeapProfiles desc,
-      perfSampleCount desc,
-      total_dur desc,
-      sliceCount desc,
-      processName asc nulls last,
-      the_tracks.upid asc nulls last,
-      threadName asc nulls last,
-      the_tracks.utid asc nulls last;
+        select
+          upid,
+          null as utid,
+          pid,
+          null as tid,
+          processName,
+          null as threadName,
+          sumRunningDur > 0 as hasSched,
+          heapProfileAllocationCount > 0
+            or heapGraphObjectCount > 0 as hasHeapInfo,
+          ifnull(chromeProcessLabels, '') as chromeProcessLabels
+        from processGroups
+        order by
+          chromeProcessRank desc,
+          heapProfileAllocationCount desc,
+          heapGraphObjectCount desc,
+          perfSampleCount desc,
+          sumRunningDur desc,
+          sliceCount desc,
+          processName asc,
+          upid asc
+      )
+      union all
+      select *
+      from (
+        select
+          null,
+          utid,
+          null as pid,
+          tid,
+          null as processName,
+          threadName,
+          sumRunningDur > 0 as hasSched,
+          0 as hasHeapInfo,
+          '' as chromeProcessLabels
+        from threadGroups
+        order by
+          perfSampleCount desc,
+          sumRunningDur desc,
+          sliceCount desc,
+          threadName asc,
+          utid asc
+      )
   `);
 
     const it = result.iter({
-      utid: NUM,
       upid: NUM_NULL,
-      tid: NUM_NULL,
+      utid: NUM_NULL,
       pid: NUM_NULL,
-      threadName: STR_NULL,
+      tid: NUM_NULL,
       processName: STR_NULL,
+      threadName: STR_NULL,
       hasSched: NUM_NULL,
-      hasHeapProfiles: NUM_NULL,
+      hasHeapInfo: NUM_NULL,
       chromeProcessLabels: STR,
     });
     for (; it.valid(); it.next()) {
       const utid = it.utid;
-      const tid = it.tid;
       const upid = it.upid;
       const pid = it.pid;
+      const tid = it.tid;
       const threadName = it.threadName;
       const processName = it.processName;
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
       const hasSched = !!it.hasSched;
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const hasHeapProfiles = !!it.hasHeapProfiles;
+      const hasHeapInfo = !!it.hasHeapInfo;
 
-      // Group by upid if present else by utid.
-      let pUuid =
-        upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
-      // These should only happen once for each track group.
-      if (pUuid === undefined) {
-        pUuid = this.getOrCreateUuid(utid, upid);
-        const summaryTrackKey = uuidv4();
-        const type = hasSched ? 'schedule' : 'summary';
-        const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
+      const summaryTrackKey = uuidv4();
+      const type = hasSched ? 'schedule' : 'summary';
+      const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
 
-        this.tracksToAdd.push({
-          uri,
-          key: summaryTrackKey,
-          trackSortKey: hasSched
-            ? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK
-            : PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
-          name: `${upid === null ? tid : pid} summary`,
-          labels: it.chromeProcessLabels.split(','),
-        });
-
-        const name = getTrackName({
-          utid,
-          processName,
-          pid,
-          threadName,
-          tid,
-          upid,
-        });
-        const addTrackGroup = Actions.addTrackGroup({
-          summaryTrackKey,
-          name,
-          id: pUuid,
-          // Perf profiling tracks remain collapsed, otherwise we would have too
-          // many expanded process tracks for some perf traces, leading to
-          // jankyness.
-          collapsed: !hasHeapProfiles,
-        });
-
-        this.addTrackGroupActions.push(addTrackGroup);
+      // If previous groupings (e.g. kernel threads) picked up there tracks,
+      // don't try to regroup them.
+      const pUuid =
+        upid === null ? this.utidToUuid.get(utid!) : this.upidToUuid.get(upid);
+      if (pUuid !== undefined) {
+        continue;
       }
+
+      this.tracksToAdd.push({
+        uri,
+        key: summaryTrackKey,
+        trackSortKey: hasSched
+          ? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK
+          : PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
+        name: `${upid === null ? tid : pid} summary`,
+        labels: it.chromeProcessLabels.split(','),
+      });
+
+      const name = getTrackName({
+        utid,
+        processName,
+        pid,
+        threadName,
+        tid,
+        upid,
+      });
+      const addTrackGroup = Actions.addTrackGroup({
+        summaryTrackKey,
+        name,
+        id: this.getOrCreateUuid(utid, upid),
+        // Perf profiling tracks remain collapsed, otherwise we would have too
+        // many expanded process tracks for some perf traces, leading to
+        // jankyness.
+        collapsed: !hasHeapInfo,
+      });
+      this.addTrackGroupActions.push(addTrackGroup);
     }
   }
 
diff --git a/ui/src/core/colorizer.ts b/ui/src/core/colorizer.ts
index 3850e1e..3f0d1b3 100644
--- a/ui/src/core/colorizer.ts
+++ b/ui/src/core/colorizer.ts
@@ -182,6 +182,8 @@
       return DESAT_RED;
     }
     return ORANGE;
+  } else if (state.includes('Dead')) {
+    return GRAY;
   } else if (state.includes('Sleeping') || state.includes('Idle')) {
     return TRANSPARENT_WHITE;
   }
diff --git a/ui/src/core/timeline_cache.ts b/ui/src/core/timeline_cache.ts
index 30d1cd3..fe84876 100644
--- a/ui/src/core/timeline_cache.ts
+++ b/ui/src/core/timeline_cache.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {BigintMath} from '../base/bigint_math';
-import {assertTrue} from '../base/logging';
 import {duration, time, Time} from '../base/time';
 
 export const BUCKETS_PER_PIXEL = 2;
@@ -123,63 +122,3 @@
     return `CacheKey<${start}, ${end}, ${bucket}, ${size}>`;
   }
 }
-
-interface CacheItem<T> {
-  t: T;
-  lastAccessId: number;
-}
-
-// LRU cache for the timeline.
-// T is all the data needed for a displaying the track in a given
-// CacheKey area - generally an array of slices.
-export class TimelineCache<T> {
-  private cacheSize: number;
-  private cache: Map<string, CacheItem<T>>;
-  private lastAccessId: number;
-
-  constructor(cacheSize: number) {
-    assertTrue(cacheSize >= 2);
-    this.cacheSize = cacheSize;
-    this.cache = new Map();
-    this.lastAccessId = 0;
-  }
-
-  invalidate() {
-    this.cache.clear();
-  }
-
-  insert(cacheKey: CacheKey, t: T): void {
-    assertTrue(cacheKey.isNormalized());
-    const key = cacheKey.toString();
-    this.cache.set(key, {
-      t,
-      lastAccessId: this.lastAccessId++,
-    });
-    this.updateLru();
-  }
-
-  lookup(cacheKey: CacheKey): undefined | T {
-    assertTrue(cacheKey.isNormalized());
-    const key = cacheKey.toString();
-    const item = this.cache.get(key);
-    if (item) {
-      item.lastAccessId = this.lastAccessId++;
-      this.updateLru();
-    }
-    return item === undefined ? undefined : item.t;
-  }
-
-  private updateLru(): void {
-    while (this.cache.size > this.cacheSize) {
-      let oldestKey = '';
-      let oldestAccessId = Number.MAX_SAFE_INTEGER;
-      for (const [k, v] of this.cache.entries()) {
-        if (v.lastAccessId < oldestAccessId) {
-          oldestAccessId = v.lastAccessId;
-          oldestKey = k;
-        }
-      }
-      this.cache.delete(oldestKey);
-    }
-  }
-}
diff --git a/ui/src/core/timeline_cache_unittest.ts b/ui/src/core/timeline_cache_unittest.ts
index 227c19d..c41fb64 100644
--- a/ui/src/core/timeline_cache_unittest.ts
+++ b/ui/src/core/timeline_cache_unittest.ts
@@ -14,7 +14,7 @@
 
 import {Time} from '../base/time';
 
-import {CacheKey, TimelineCache} from './timeline_cache';
+import {CacheKey} from './timeline_cache';
 
 test('cacheKeys', () => {
   const k = CacheKey.create(Time.fromRaw(201n), Time.fromRaw(302n), 123);
@@ -29,62 +29,3 @@
   expect(n.bucketSize).toBeGreaterThanOrEqual(k.bucketSize);
   expect(Math.abs(n.windowSizePx - k.windowSizePx)).toBeLessThanOrEqual(200);
 });
-
-test('cache', () => {
-  const key1 = CacheKey.create(
-    Time.fromRaw(1000n),
-    Time.fromRaw(1100n),
-    100,
-  ).normalize();
-  const key2 = CacheKey.create(
-    Time.fromRaw(2000n),
-    Time.fromRaw(2100n),
-    100,
-  ).normalize();
-  const key3 = CacheKey.create(
-    Time.fromRaw(3000n),
-    Time.fromRaw(3100n),
-    100,
-  ).normalize();
-  const key4 = CacheKey.create(
-    Time.fromRaw(4000n),
-    Time.fromRaw(4100n),
-    100,
-  ).normalize();
-  const key5 = CacheKey.create(
-    Time.fromRaw(5000n),
-    Time.fromRaw(5100n),
-    100,
-  ).normalize();
-  const key6 = CacheKey.create(
-    Time.fromRaw(6000n),
-    Time.fromRaw(6100n),
-    100,
-  ).normalize();
-  const key7 = CacheKey.create(
-    Time.fromRaw(7000n),
-    Time.fromRaw(7100n),
-    100,
-  ).normalize();
-  const cache = new TimelineCache<string>(5);
-
-  cache.insert(key1, 'v1');
-  expect(cache.lookup(key1)).toEqual('v1');
-
-  cache.insert(key2, 'v2');
-  cache.insert(key3, 'v3');
-  cache.insert(key4, 'v4');
-  cache.insert(key5, 'v5');
-
-  // Should push key1/v1 out of the cache:
-  cache.insert(key6, 'v6');
-  expect(cache.lookup(key1)).toEqual(undefined);
-
-  // Access key2 then add one more entry:
-  expect(cache.lookup(key2)).toEqual('v2');
-  cache.insert(key7, 'v7');
-
-  // key2/v2 should still be present but key3/v3 should be discarded:
-  expect(cache.lookup(key2)).toEqual('v2');
-  expect(cache.lookup(key3)).toEqual(undefined);
-});
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index c6c46f6..d24fc1d 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -109,7 +109,6 @@
         key,
         label: name,
         active: currentViewKey === key,
-        minimal: true,
       });
     });
 
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 4bed3fc..dc65d19 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -414,7 +414,7 @@
     },
     {
       id: 'perfetto.ShowSliceTable',
-      name: 'Show slice table',
+      name: 'Open new slice table tab',
       callback: () => {
         addSqlTableTab({
           table: SqlTables.slice,
@@ -452,13 +452,13 @@
     },
     {
       id: 'perfetto.OpenCommandPalette',
-      name: 'Open Command Palette',
+      name: 'Open command palette',
       callback: () => this.enterCommandMode(),
       defaultHotkey: '!Mod+Shift+P',
     },
     {
       id: 'perfetto.RunQuery',
-      name: 'Run Query',
+      name: 'Run query',
       callback: () => this.enterQueryMode(),
       defaultHotkey: '!Mod+O',
     },
@@ -545,7 +545,7 @@
     },
     {
       id: 'perfetto.FocusSelection',
-      name: 'Focus selection',
+      name: 'Focus current selection',
       callback: () => findCurrentSelection(),
       defaultHotkey: 'F',
     },
@@ -804,6 +804,9 @@
         this.addRecentCommand(key);
         cmdMgr.runCommand(key);
       },
+      onGoBack: () => {
+        this.enterSearchMode(false);
+      },
     });
   }
 
@@ -822,6 +825,7 @@
       placeholder: ph,
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'query-mode',
+
       onInput: (value) => {
         this.queryText = value;
         raf.scheduleFullRedraw();
@@ -842,6 +846,9 @@
         this.enterSearchMode(false);
         raf.scheduleFullRedraw();
       },
+      onGoBack: () => {
+        this.enterSearchMode(false);
+      },
     });
   }
 
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index e87f38b..91b0939 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -17,7 +17,7 @@
 import {searchSegment} from '../base/binary_search';
 import {Disposable, NullDisposable} from '../base/disposable';
 import {assertTrue, assertUnreachable} from '../base/logging';
-import {duration, Time, time} from '../base/time';
+import {Time, time} from '../base/time';
 import {drawTrackHoverTooltip} from '../common/canvas_utils';
 import {raf} from '../core/raf_scheduler';
 import {EngineProxy, LONG, NUM, Track} from '../public';
@@ -27,10 +27,10 @@
 import {checkerboardExcept} from './checkerboard';
 import {globals} from './globals';
 import {PanelSize} from './panel';
-import {constraintsToQuerySuffix} from './sql_utils';
 import {NewTrackArgs} from './track';
-import {CacheKey, TimelineCache} from '../core/timeline_cache';
+import {CacheKey} from '../core/timeline_cache';
 import {featureFlags} from '../core/feature_flags';
+import {uuidv4Sql} from '../base/uuid';
 
 export const COUNTER_DEBUG_MENU_ITEMS = featureFlags.register({
   id: 'counterDebugMenuItems',
@@ -128,8 +128,6 @@
 
 interface CounterData {
   timestamps: BigInt64Array;
-  counts: Uint32Array;
-  avgValues: Float64Array;
   minDisplayValues: Float64Array;
   maxDisplayValues: Float64Array;
   lastDisplayValues: Float64Array;
@@ -142,13 +140,10 @@
 interface CounterLimits {
   maxDisplayValue: number;
   minDisplayValue: number;
-  maxDurNs: duration;
 }
 
 interface CounterTooltipState {
   lastDisplayValue: number;
-  avgValue: number;
-  count: number;
   ts: time;
   tsEnd?: time;
 }
@@ -201,22 +196,19 @@
 export abstract class BaseCounterTrack implements Track {
   protected engine: EngineProxy;
   protected trackKey: string;
+  protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
   private countersKey: CacheKey = CacheKey.zero();
 
   private counters: CounterData = {
     timestamps: new BigInt64Array(0),
-    counts: new Uint32Array(0),
-    avgValues: new Float64Array(0),
     minDisplayValues: new Float64Array(0),
     maxDisplayValues: new Float64Array(0),
     lastDisplayValues: new Float64Array(0),
     displayValueRange: [0, 0],
   };
 
-  private cache: TimelineCache<CounterData> = new TimelineCache(5);
-
   // Cleanup hook for onInit.
   private initState?: Disposable;
 
@@ -427,12 +419,9 @@
 
   protected invalidate() {
     this.limits = undefined;
-    this.cache.invalidate();
     this.countersKey = CacheKey.zero();
     this.counters = {
       timestamps: new BigInt64Array(0),
-      counts: new Uint32Array(0),
-      avgValues: new Float64Array(0),
       minDisplayValues: new Float64Array(0),
       maxDisplayValues: new Float64Array(0),
       lastDisplayValues: new Float64Array(0),
@@ -450,7 +439,7 @@
     return m(
       PopupMenu2,
       {
-        trigger: m(Button, {icon: 'show_chart', minimal: true, compact: true}),
+        trigger: m(Button, {icon: 'show_chart', compact: true}),
       },
       this.getCounterContextMenuItems(),
     );
@@ -462,6 +451,33 @@
 
   async onCreate(): Promise<void> {
     this.initState = await this.onInit();
+
+    const displayValueQuery = await this.engine.query(`
+        create virtual table ${this.getTableName()}
+        using __intrinsic_counter_mipmap((
+          SELECT
+            ts,
+            ${this.getValueExpression()} as value
+          FROM (${this.getSqlSource()})
+        ));
+
+        select
+          min_value as minDisplayValue,
+          max_value as maxDisplayValue
+        from ${this.getTableName()}(
+          trace_start(), trace_end(), trace_dur()
+        );
+      `);
+
+    const {minDisplayValue, maxDisplayValue} = displayValueQuery.firstRow({
+      minDisplayValue: NUM,
+      maxDisplayValue: NUM,
+    });
+
+    this.limits = {
+      minDisplayValue,
+      maxDisplayValue,
+    };
   }
 
   async onUpdate(): Promise<void> {
@@ -479,10 +495,7 @@
   }
 
   render(ctx: CanvasRenderingContext2D, size: PanelSize) {
-    const {
-      visibleTimeScale: timeScale,
-      // visibleWindowTime: vizTime,
-    } = globals.timeline;
+    const {visibleTimeScale: timeScale} = globals.timeline;
 
     // In any case, draw whatever we have (which might be stale/incomplete).
 
@@ -490,11 +503,17 @@
     const data = this.counters;
 
     if (data.timestamps.length === 0 || limits === undefined) {
+      checkerboardExcept(
+        ctx,
+        this.getHeight(),
+        0,
+        size.width,
+        timeScale.timeToPx(this.countersKey.start),
+        timeScale.timeToPx(this.countersKey.end),
+      );
       return;
     }
 
-    assertTrue(data.timestamps.length === data.counts.length);
-    assertTrue(data.timestamps.length === data.avgValues.length);
     assertTrue(data.timestamps.length === data.minDisplayValues.length);
     assertTrue(data.timestamps.length === data.maxDisplayValues.length);
     assertTrue(data.timestamps.length === data.lastDisplayValues.length);
@@ -520,16 +539,9 @@
       zeroY = effectiveHeight * (yMax / (yMax - yMin)) + MARGIN_TOP;
     }
 
-    // There are 360deg of hue. We want a scale that starts at green with
-    // exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet
-    // around exp >= 9 (1GB).
-    // The hue scale looks like this:
-    // 0                              180                                 360
-    // Red        orange         green | blue         purple          magenta
-    // So we want to start @ 180deg with pow=0, go down to 0deg and then wrap
-    // back from 360deg back to 180deg.
+    // Use hue to differentiate the scale of the counter value
     const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
-    const expCapped = Math.min(Math.max(exp - 3), 9);
+    const expCapped = Math.min(exp - 3, 9);
     const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360;
 
     ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
@@ -548,11 +560,11 @@
 
     ctx.beginPath();
     const timestamp = Time.fromRaw(timestamps[0]);
-    ctx.moveTo(calculateX(timestamp), zeroY);
+    ctx.moveTo(Math.max(0, calculateX(timestamp)), zeroY);
     let lastDrawnY = zeroY;
     for (let i = 0; i < timestamps.length; i++) {
       const timestamp = Time.fromRaw(timestamps[i]);
-      const x = calculateX(timestamp);
+      const x = Math.max(0, calculateX(timestamp));
       const minY = calculateY(minValues[i]);
       const maxY = calculateY(maxValues[i]);
       const lastY = calculateY(lastValues[i]);
@@ -589,7 +601,7 @@
 
     const hover = this.hover;
     if (hover !== undefined) {
-      let text = `${hover.avgValue.toLocaleString()}`;
+      let text = `${hover.lastDisplayValue.toLocaleString()}`;
 
       const unit = this.unit;
       switch (options.yMode) {
@@ -607,14 +619,11 @@
           break;
       }
 
-      if (hover.count > 1) {
-        text += ` (avg of ${hover.count})`;
-      }
-
       ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
       ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
 
-      const xStart = Math.floor(timeScale.timeToPx(hover.ts));
+      const rawXStart = calculateX(hover.ts);
+      const xStart = Math.max(0, rawXStart);
       const xEnd =
         hover.tsEnd === undefined
           ? endPx
@@ -634,17 +643,19 @@
       ctx.stroke();
       ctx.lineWidth = 1;
 
-      // Draw change marker.
-      ctx.beginPath();
-      ctx.arc(
-        xStart,
-        y,
-        3 /* r*/,
-        0 /* start angle*/,
-        2 * Math.PI /* end angle*/,
-      );
-      ctx.fill();
-      ctx.stroke();
+      // Draw change marker if it would be visible.
+      if (rawXStart >= -6) {
+        ctx.beginPath();
+        ctx.arc(
+          xStart,
+          y,
+          3 /* r*/,
+          0 /* start angle*/,
+          2 * Math.PI /* end angle*/,
+        );
+        ctx.fill();
+        ctx.stroke();
+      }
 
       // Draw the tooltip.
       drawTrackHoverTooltip(ctx, this.mousePos, this.getHeight(), text);
@@ -652,11 +663,11 @@
 
     // Write the Y scale on the top left corner.
     ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
-    ctx.fillRect(0, 0, 42, 16);
+    ctx.fillRect(0, 0, 42, 13);
     ctx.fillStyle = '#666';
     ctx.textAlign = 'left';
     ctx.textBaseline = 'alphabetic';
-    ctx.fillText(`${yLabel}`, 5, 14);
+    ctx.fillText(`${yLabel}`, 5, 11);
 
     // TODO(hjd): Refactor this into checkerboardExcept
     {
@@ -698,14 +709,10 @@
     const tsEnd =
       right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
     const lastDisplayValue = data.lastDisplayValues[left];
-    const count = data.counts[left];
-    const avgValue = data.avgValues[left];
     this.hover = {
       ts,
       tsEnd,
       lastDisplayValue,
-      count,
-      avgValue,
     };
   }
 
@@ -713,11 +720,14 @@
     this.hover = undefined;
   }
 
-  onDestroy(): void {
+  async onDestroy(): Promise<void> {
     if (this.initState) {
       this.initState.dispose();
       this.initState = undefined;
     }
+    if (this.engine.isAlive) {
+      await this.engine.query(`drop table if exists ${this.getTableName()}`);
+    }
   }
 
   // Compute the range of values to display and range label.
@@ -792,7 +802,6 @@
         break;
       default:
         assertUnreachable(options.yMode);
-        break;
     }
 
     if (options.yDisplay === 'log') {
@@ -808,11 +817,10 @@
   }
 
   // The underlying table has `ts` and `value` columns.
-  private getSqlPreamble(): string {
+  private getValueExpression(): string {
     const options = this.getCounterOptions();
 
     let valueExpr;
-
     switch (options.yMode) {
       case 'value':
         valueExpr = 'value';
@@ -826,64 +834,20 @@
         break;
       default:
         assertUnreachable(options.yMode);
-        break;
     }
 
-    let displayValueExpr = valueExpr;
     if (options.yDisplay === 'log') {
-      displayValueExpr = `ifnull(ln(${displayValueExpr}), 0)`;
+      return `ifnull(ln(${valueExpr}), 0)`;
+    } else {
+      return valueExpr;
     }
+  }
 
-    return `
-      WITH data AS (
-        SELECT
-          ts,
-          ${valueExpr} as value,
-          ${displayValueExpr} as displayValue
-        FROM (${this.getSqlSource()})
-      )
-    `;
+  private getTableName(): string {
+    return `counter_${this.trackUuid}`;
   }
 
   private async maybeRequestData(rawCountersKey: CacheKey) {
-    let limits = this.limits;
-    if (limits === undefined) {
-      const maxDurQuery = await this.engine.query(`
-        ${this.getSqlPreamble()}
-        SELECT
-          max(dur) as maxDur
-        FROM (
-          SELECT
-            lead(ts, 1, ts) over (order by ts) - ts as dur
-          FROM data
-        )
-      `);
-      const maxDurRow = maxDurQuery.firstRow({
-        maxDur: LONG,
-      });
-      const maxDurNs = maxDurRow.maxDur;
-
-      const displayValueQuery = await this.engine.query(`
-        ${this.getSqlPreamble()}
-        SELECT
-          max(displayValue) as maxDisplayValue,
-          min(displayValue) as minDisplayValue
-        FROM data
-      `);
-      const displayValueRow = displayValueQuery.firstRow({
-        minDisplayValue: NUM,
-        maxDisplayValue: NUM,
-      });
-
-      const minDisplayValue = displayValueRow.minDisplayValue;
-      const maxDisplayValue = displayValueRow.maxDisplayValue;
-      limits = this.limits = {
-        minDisplayValue,
-        maxDisplayValue,
-        maxDurNs,
-      };
-    }
-
     if (rawCountersKey.isCoveredBy(this.countersKey)) {
       return; // We have the data already, no need to re-query.
     }
@@ -895,41 +859,21 @@
       );
     }
 
-    const maybeCachedCounters = this.cache.lookup(countersKey);
-    if (maybeCachedCounters) {
-      this.countersKey = countersKey;
-      this.counters = maybeCachedCounters;
-    }
-
-    const bucketNs = countersKey.bucketSize;
-
-    const constraint = constraintsToQuerySuffix({
-      filters: [
-        `ts >= ${countersKey.start} - ${limits.maxDurNs}`,
-        `ts <= ${countersKey.end}`,
-        `value is not null`,
-      ],
-      groupBy: ['tsq'],
-      orderBy: ['tsq'],
-    });
-
     const queryRes = await this.engine.query(`
-      ${this.getSqlPreamble()}
       SELECT
-        (ts + ${bucketNs / 2n}) / ${bucketNs} * ${bucketNs} as tsq,
-        count(value) as count,
-        avg(value) as avgValue,
-        min(displayValue) as minDisplayValue,
-        max(displayValue) as maxDisplayValue,
-        value_at_max_ts(ts, displayValue) as lastDisplayValue
-      FROM data
-      ${constraint}
+        min_value as minDisplayValue,
+        max_value as maxDisplayValue,
+        last_ts as ts,
+        last_value as lastDisplayValue
+      FROM ${this.getTableName()}(
+        ${countersKey.start},
+        ${countersKey.end},
+        ${countersKey.bucketSize}
+      );
     `);
 
     const it = queryRes.iter({
-      tsq: LONG,
-      count: NUM,
-      avgValue: NUM,
+      ts: LONG,
       minDisplayValue: NUM,
       maxDisplayValue: NUM,
       lastDisplayValue: NUM,
@@ -938,8 +882,6 @@
     const numRows = queryRes.numRows();
     const data: CounterData = {
       timestamps: new BigInt64Array(numRows),
-      counts: new Uint32Array(numRows),
-      avgValues: new Float64Array(numRows),
       minDisplayValues: new Float64Array(numRows),
       maxDisplayValues: new Float64Array(numRows),
       lastDisplayValues: new Float64Array(numRows),
@@ -949,10 +891,7 @@
     let min = 0;
     let max = 0;
     for (let row = 0; it.valid(); it.next(), row++) {
-      const ts = Time.fromRaw(it.tsq);
-      data.timestamps[row] = ts;
-      data.counts[row] = it.count;
-      data.avgValues[row] = it.avgValue;
+      data.timestamps[row] = Time.fromRaw(it.ts);
       data.minDisplayValues[row] = it.minDisplayValue;
       data.maxDisplayValues[row] = it.maxDisplayValue;
       data.lastDisplayValues[row] = it.lastDisplayValue;
@@ -962,13 +901,13 @@
 
     data.displayValueRange = [min, max];
 
-    this.cache.insert(countersKey, data);
+    this.countersKey = countersKey;
     this.counters = data;
 
     raf.scheduleRedraw();
   }
 
   get unit(): string {
-    return this.getCounterOptions().unit ?? '?';
+    return this.getCounterOptions().unit ?? '';
   }
 }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index ef3b60b..e4b03a2 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -39,13 +39,9 @@
 import {globals} from './globals';
 import {PanelSize} from './panel';
 import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
-import {constraintsToQuerySuffix} from './sql_utils';
 import {NewTrackArgs} from './track';
-import {
-  BUCKETS_PER_PIXEL,
-  CacheKey,
-  TimelineCache,
-} from '../core/timeline_cache';
+import {BUCKETS_PER_PIXEL, CacheKey} from '../core/timeline_cache';
+import {uuidv4Sql} from '../base/uuid';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -125,13 +121,11 @@
   // to the right).
   // Since the slices are sorted by startS we can check this easily:
   const maybeFirstSlice: S | undefined = slices[0];
-  if (exists(maybeFirstSlice) && maybeFirstSlice.startNsQ > end) {
+  if (exists(maybeFirstSlice) && maybeFirstSlice.startNs > end) {
     return [];
   }
 
-  return slices.filter(
-    (slice) => slice.startNsQ <= end && slice.endNsQ >= start,
-  );
+  return slices.filter((slice) => slice.startNs <= end && slice.endNs >= start);
 }
 
 export const filterVisibleSlicesForTesting = filterVisibleSlices;
@@ -148,10 +142,6 @@
   ts: LONG, // Start time in nanoseconds.
   dur: LONG, // Duration in nanoseconds. -1 = incomplete, 0 = instant.
   depth: NUM, // Vertical depth.
-
-  // These are computed by the base class:
-  tsq: LONG, // Quantized |ts|. This class owns the quantization logic.
-  tsqEnd: LONG, // Quantized |ts+dur|. The end bucket.
 };
 
 export type BaseRow = typeof BASE_ROW;
@@ -185,6 +175,7 @@
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
   protected engine: EngineProxy;
   protected trackKey: string;
+  protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
   private slicesKey: CacheKey = CacheKey.zero();
@@ -192,17 +183,11 @@
   // This is the currently 'cached' slices:
   private slices = new Array<CastInternal<T['slice']>>();
 
-  // This is the slices cache:
-  private cache: TimelineCache<Array<CastInternal<T['slice']>>> =
-    new TimelineCache(5);
-
-  private hasOneOffData: boolean = false;
   // Incomplete slices (dur = -1). Rather than adding a lot of logic to
   // the SQL queries to handle this case we materialise them one off
   // then unconditionally render them. This should be efficient since
   // there are at most |depth| slices.
   private incomplete = new Array<CastInternal<T['slice']>>();
-  private maxDurNs: duration = 0n;
 
   // The currently selected slice.
   // TODO(hjd): We should fetch this from the underlying data rather
@@ -324,8 +309,67 @@
     return `${size}px Roboto Condensed`;
   }
 
+  private getTableName(): string {
+    return `slice_${this.trackUuid}`;
+  }
+
   async onCreate(): Promise<void> {
     this.initState = await this.onInit();
+
+    // TODO(hjd): Consider case below:
+    // raw:
+    // 0123456789
+    //   [A     did not end)
+    //     [B ]
+    //
+    //
+    // quantised:
+    // 0123456789
+    //   [A     did not end)
+    // [     B  ]
+    // Does it lead to odd results?
+    const extraCols = this.extraSqlColumns.join(',');
+    let queryRes;
+    if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
+      queryRes = await this.engine.query(`
+          select
+            ${this.depthColumn()},
+            ts,
+            -1 as dur,
+            id
+            ${extraCols ? ',' + extraCols : ''}
+          from (${this.getSqlSource()})
+          where dur = -1;
+        `);
+    } else {
+      queryRes = await this.engine.query(`
+        select
+          ${this.depthColumn()},
+          max(ts) as ts,
+          -1 as dur,
+          id
+          ${extraCols ? ',' + extraCols : ''}
+        from (${this.getSqlSource()})
+        where dur = -1
+        group by 1
+      `);
+    }
+    const incomplete = new Array<CastInternal<T['slice']>>(queryRes.numRows());
+    const it = queryRes.iter(this.getRowSpec());
+    for (let i = 0; it.valid(); it.next(), ++i) {
+      incomplete[i] = this.rowToSliceInternal(it);
+    }
+    this.onUpdatedSlices(incomplete);
+    this.incomplete = incomplete;
+
+    await this.engine.query(`
+      create virtual table ${this.getTableName()}
+      using __intrinsic_slice_mipmap((
+        select id, ts, dur, ${this.depthColumn()}
+        from (${this.getSqlSource()})
+        where dur != -1
+      ));
+    `);
   }
 
   async onUpdate(): Promise<void> {
@@ -394,8 +438,8 @@
       // partially visible. This might end up with a negative x if the
       // slice starts before the visible time or with a width that overflows
       // pxEnd.
-      slice.x = timeScale.timeToPx(slice.startNsQ);
-      slice.w = timeScale.durationToPx(slice.durNsQ);
+      slice.x = timeScale.timeToPx(slice.startNs);
+      slice.w = timeScale.durationToPx(slice.durNs);
 
       if (slice.flags & SLICE_FLAGS_INSTANT) {
         // In the case of an instant slice, set the slice geometry on the
@@ -611,82 +655,20 @@
     } // if (hoveredSlice)
   }
 
-  onDestroy() {
+  async onDestroy(): Promise<void> {
     if (this.initState) {
       this.initState.dispose();
       this.initState = undefined;
     }
+    if (this.engine.isAlive) {
+      await this.engine.execute(`drop table ${this.getTableName()}`);
+    }
   }
 
   // This method figures out if the visible window is outside the bounds of
   // the cached data and if so issues new queries (i.e. sorta subsumes the
   // onBoundsChange).
   private async maybeRequestData(rawSlicesKey: CacheKey) {
-    if (!this.hasOneOffData) {
-      // TODO(hjd): This could be done in onInit maybe?
-      const queryRes = await this.engine.query(`select
-          ifnull(max(dur), 0) as maxDur, count(1) as rowCount
-          from (${this.getSqlSource()})`);
-      const row = queryRes.firstRow({maxDur: LONG, rowCount: NUM});
-      this.maxDurNs = row.maxDur;
-
-      {
-        // TODO(hjd): Consider case below:
-        // raw:
-        // 0123456789
-        //   [A     did not end)
-        //     [B ]
-        //
-        //
-        // quantised:
-        // 0123456789
-        //   [A     did not end)
-        // [     B  ]
-        // Does it lead to odd results?
-        const extraCols = this.extraSqlColumns.join(',');
-        let queryRes;
-        if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
-          queryRes = await this.engine.query(`
-            select
-              ${this.depthColumn()},
-              ts as tsq,
-              ts as tsqEnd,
-              ts,
-              -1 as dur,
-              id
-              ${extraCols ? ',' + extraCols : ''}
-            from (${this.getSqlSource()})
-            where dur = -1;
-          `);
-        } else {
-          queryRes = await this.engine.query(`
-            select
-              ${this.depthColumn()},
-              max(ts) as tsq,
-              max(ts) as tsqEnd,
-              max(ts) as ts,
-              -1 as dur,
-              id
-              ${extraCols ? ',' + extraCols : ''}
-            from (${this.getSqlSource()})
-            group by 1
-            having dur = -1;
-          `);
-        }
-        const incomplete = new Array<CastInternal<T['slice']>>(
-          queryRes.numRows(),
-        );
-        const it = queryRes.iter(this.getRowSpec());
-        for (let i = 0; it.valid(); it.next(), ++i) {
-          incomplete[i] = this.rowToSliceInternal(it);
-        }
-        this.onUpdatedSlices(incomplete);
-        this.incomplete = incomplete;
-      }
-
-      this.hasOneOffData = true;
-    }
-
     if (rawSlicesKey.isCoveredBy(this.slicesKey)) {
       return; // We have the data already, no need to re-query
     }
@@ -699,52 +681,20 @@
       );
     }
 
-    const maybeCachedSlices = this.cache.lookup(slicesKey);
-    if (maybeCachedSlices) {
-      this.slicesKey = slicesKey;
-      this.onUpdatedSlices(maybeCachedSlices);
-      this.slices = maybeCachedSlices;
-      return;
-    }
-
-    const bucketNs = slicesKey.bucketSize;
-    let queryTsq;
-    let queryTsqEnd;
-    // When we're zoomed into the level of single ns there is no point
-    // doing quantization (indeed it causes bad artifacts) so instead
-    // we use ts / ts+dur directly.
-    if (bucketNs === 1n) {
-      queryTsq = 'ts';
-      queryTsqEnd = 'ts + dur';
-    } else {
-      queryTsq = `(ts + ${bucketNs / 2n}) / ${bucketNs} * ${bucketNs}`;
-      queryTsqEnd = `(ts + dur + ${bucketNs / 2n}) / ${bucketNs} * ${bucketNs}`;
-    }
-
     const extraCols = this.extraSqlColumns.join(',');
-    const maybeDepth = this.isFlat() ? undefined : 'depth';
-
-    const constraint = constraintsToQuerySuffix({
-      filters: [
-        `ts >= ${slicesKey.start - this.maxDurNs}`,
-        `ts <= ${slicesKey.end}`,
-      ],
-      groupBy: [maybeDepth, 'tsq'],
-      orderBy: [maybeDepth, 'tsq'],
-    });
-
-    // TODO(hjd): Count and expose the number of slices summarized in
-    // each bucket?
     const queryRes = await this.engine.query(`
       SELECT
-        ${queryTsq} AS tsq,
-        ${queryTsqEnd} AS tsqEnd,
-        ts,
-        MAX(dur) AS dur,
-        id,
-        ${this.depthColumn()}
+        (z.ts / ${rawSlicesKey.bucketSize}) * ${rawSlicesKey.bucketSize} as ts,
+        iif(s.dur = -1, s.dur, max(z.dur, ${rawSlicesKey.bucketSize})) as dur,
+        s.id,
+        z.depth
         ${extraCols ? ',' + extraCols : ''}
-      FROM (${this.getSqlSource()}) ${constraint}
+      FROM ${this.getTableName()}(
+        ${slicesKey.start},
+        ${slicesKey.end},
+        ${slicesKey.bucketSize}
+      ) z
+      CROSS JOIN (${this.getSqlSource()}) s using (id)
     `);
 
     // Here convert each row to a Slice. We do what we can do
@@ -768,7 +718,6 @@
     }
     this.maxDataDepth = maxDataDepth;
     this.onUpdatedSlices(slices);
-    this.cache.insert(slicesKey, slices);
     this.slices = slices;
 
     raf.scheduleRedraw();
@@ -789,8 +738,8 @@
   }
 
   rowToSlice(row: T['row']): T['slice'] {
-    const startNsQ = Time.fromRaw(row.tsq);
-    const endNsQ = Time.fromRaw(row.tsqEnd);
+    const startNs = Time.fromRaw(row.ts);
+    const endNs = Time.fromRaw(row.ts + row.dur);
     const ts = Time.fromRaw(row.ts);
     const dur: duration = row.dur;
 
@@ -803,9 +752,9 @@
 
     return {
       id: row.id,
-      startNsQ,
-      endNsQ,
-      durNsQ: endNsQ - startNsQ,
+      startNs,
+      endNs,
+      durNs: row.dur,
       ts,
       dur,
       flags,
@@ -846,7 +795,7 @@
     for (const slice of this.incomplete) {
       const visibleTimeScale = globals.timeline.visibleTimeScale;
       const startPx = CROP_INCOMPLETE_SLICE_FLAG.get()
-        ? visibleTimeScale.timeToPx(slice.startNsQ)
+        ? visibleTimeScale.timeToPx(slice.startNs)
         : slice.x;
       const cropUnfinishedSlicesCondition = CROP_INCOMPLETE_SLICE_FLAG.get()
         ? startPx + INCOMPLETE_SLICE_WIDTH_PX >= x
diff --git a/ui/src/frontend/base_slice_track_unittest.ts b/ui/src/frontend/base_slice_track_unittest.ts
index 196cc2e..4e8fd51 100644
--- a/ui/src/frontend/base_slice_track_unittest.ts
+++ b/ui/src/frontend/base_slice_track_unittest.ts
@@ -19,16 +19,16 @@
 import {filterVisibleSlicesForTesting as filterVisibleSlices} from './base_slice_track';
 
 function slice(start: number, duration: number, depth: number = 0): Slice {
-  const startNsQ = Time.fromRaw(BigInt(start));
-  const durNsQ = Time.fromRaw(BigInt(duration));
-  const endNsQ = Time.fromRaw(startNsQ + durNsQ);
+  const startNs = Time.fromRaw(BigInt(start));
+  const durNs = Time.fromRaw(BigInt(duration));
+  const endNs = Time.fromRaw(startNs + durNs);
   return {
     id: 42,
-    startNsQ,
-    endNsQ,
-    durNsQ,
-    ts: startNsQ,
-    dur: durNsQ,
+    startNs,
+    endNs,
+    durNs,
+    ts: startNs,
+    dur: durNs,
     depth,
     flags: 0,
     title: '',
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 90471ed..f42d4b6 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -399,7 +399,6 @@
     const contextMenuItems = getSliceContextMenuItems(sliceInfo);
     if (contextMenuItems.length > 0) {
       const trigger = m(Button, {
-        minimal: true,
         compact: true,
         label: 'Contextual Options',
         rightIcon: Icons.ContextMenu,
diff --git a/ui/src/frontend/close_track_button.ts b/ui/src/frontend/close_track_button.ts
index e7b667b..1b0fd91 100644
--- a/ui/src/frontend/close_track_button.ts
+++ b/ui/src/frontend/close_track_button.ts
@@ -34,7 +34,6 @@
       },
       icon: Icons.Close,
       title: 'Close',
-      minimal: true,
       compact: true,
     });
   }
diff --git a/ui/src/frontend/css_constants.ts b/ui/src/frontend/css_constants.ts
index 1a37c1e..f756136 100644
--- a/ui/src/frontend/css_constants.ts
+++ b/ui/src/frontend/css_constants.ts
@@ -23,7 +23,6 @@
 export let SELECTION_FILL_COLOR = '#8398e64d';
 export let OVERVIEW_TIMELINE_NON_VISIBLE_COLOR = '#c8c8c8cc';
 export let DEFAULT_DETAILS_CONTENT_HEIGHT = 280;
-export const SELECTED_LOG_ROWS_COLOR = '#D2EFE0';
 export let BACKGROUND_COLOR = '#ffffff';
 export let FOREGROUND_COLOR = '#222';
 export let COLLAPSED_BACKGROUND = '#ffffff';
diff --git a/ui/src/frontend/drag_handle.ts b/ui/src/frontend/drag_handle.ts
index 70aea03..dd43077 100644
--- a/ui/src/frontend/drag_handle.ts
+++ b/ui/src/frontend/drag_handle.ts
@@ -209,7 +209,6 @@
               onTabClose(key);
               event.preventDefault();
             },
-            minimal: true,
             compact: true,
             icon: 'close',
           }),
@@ -237,7 +236,6 @@
           title: 'Open fullscreen',
           disabled: this.isFullscreen,
           icon: 'vertical_align_top',
-          minimal: true,
           compact: true,
         }),
         m(Button, {
@@ -246,7 +244,6 @@
           },
           title,
           icon,
-          minimal: true,
           compact: true,
         }),
       ),
@@ -258,7 +255,6 @@
       PopupMenu2,
       {
         trigger: m(Button, {
-          minimal: true,
           compact: true,
           icon: 'more_vert',
           disabled: entries.length === 0,
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 0f4cba2..6e6f550 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -42,6 +42,7 @@
 import {ButtonBar} from '../widgets/button';
 import {DurationWidget} from './widgets/duration';
 import {DetailsShell} from '../widgets/details_shell';
+import {Intent} from '../widgets/common';
 
 const HEADER_HEIGHT = 30;
 
@@ -143,6 +144,7 @@
                 this.profileType === ProfileType.JAVA_HEAP_SAMPLES) &&
                 m(Button, {
                   icon: 'file_download',
+                  intent: Intent.Primary,
                   onclick: () => {
                     this.downloadPprof();
                   },
@@ -416,7 +418,6 @@
     return m(Button, {
       label: text,
       active,
-      minimal: true,
       onclick: () => {
         globals.dispatch(Actions.changeViewFlamegraphState({viewingOption}));
       },
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 8fc392d..68e16a5 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -276,8 +276,8 @@
   private _publishRedraw?: () => void = undefined;
 
   private _currentSearchResults: CurrentSearchResults = {
-    sliceIds: new Float64Array(0),
-    tsStarts: new BigInt64Array(0),
+    eventIds: new Float64Array(0),
+    tses: new BigInt64Array(0),
     utids: new Float64Array(0),
     trackKeys: [],
     sources: [],
@@ -648,8 +648,8 @@
     this._numQueriesQueued = 0;
     this._metricResult = undefined;
     this._currentSearchResults = {
-      sliceIds: new Float64Array(0),
-      tsStarts: new BigInt64Array(0),
+      eventIds: new Float64Array(0),
+      tses: new BigInt64Array(0),
       utids: new Float64Array(0),
       trackKeys: [],
       sources: [],
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 85cbe07..c583e2d 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -108,7 +108,6 @@
             },
             title: allCollapsed ? 'Expand all' : 'Collapse all',
             icon: allCollapsed ? 'unfold_more' : 'unfold_less',
-            minimal: true,
             compact: true,
           }),
           m(Button, {
@@ -118,7 +117,6 @@
             },
             title: 'Clear all pinned tracks',
             icon: 'clear_all',
-            minimal: true,
             compact: true,
           }),
         ),
@@ -394,7 +392,6 @@
         m(Button, {
           label: 'Remove',
           icon: Icons.Delete,
-          minimal: true,
           onclick: () => {
             globals.dispatch(Actions.removeNote({id: this.config.id}));
             raf.scheduleFullRedraw();
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index b0f0416..16ada06 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -123,6 +123,9 @@
   // Called when the user expresses the intent to "execute" the thing.
   onSubmit?: (value: string, mod: boolean, shift: boolean) => void;
 
+  // Called when the user hits backspace when the field is empty.
+  onGoBack?: () => void;
+
   // When true, disable and grey-out the omnibox's input.
   readonly?: boolean;
 
@@ -160,6 +163,7 @@
       extraClasses,
       onInput = () => {},
       onSubmit = () => {},
+      onGoBack = () => {},
       inputRef = 'omnibox',
       options,
       closeOnSubmit = false,
@@ -190,7 +194,7 @@
             },
             onkeydown: (e: KeyboardEvent) => {
               if (e.key === 'Backspace' && value === '') {
-                this.close(attrs);
+                onGoBack();
               } else if (e.key === 'Escape') {
                 e.preventDefault();
                 this.close(attrs);
diff --git a/ui/src/frontend/pivot_table_argument_popup.ts b/ui/src/frontend/pivot_table_argument_popup.ts
index faa1579..949200e 100644
--- a/ui/src/frontend/pivot_table_argument_popup.ts
+++ b/ui/src/frontend/pivot_table_argument_popup.ts
@@ -20,21 +20,6 @@
 
 interface ArgumentPopupArgs {
   onArgumentChange: (arg: string) => void;
-  knownArguments: string[];
-}
-
-function longestString(array: string[]): string {
-  if (array.length === 0) {
-    return '';
-  }
-
-  let answer = array[0];
-  for (let i = 1; i < array.length; i++) {
-    if (array[i].length > answer.length) {
-      answer = array[i];
-    }
-  }
-  return answer;
 }
 
 // Component rendering popup for entering an argument name to use as a pivot.
@@ -47,41 +32,6 @@
     raf.scheduleFullRedraw();
   }
 
-  renderMatches(attrs: ArgumentPopupArgs): m.Child[] {
-    const result: m.Child[] = [];
-
-    for (const option of attrs.knownArguments) {
-      // Would be great to have smarter fuzzy matching, but in the meantime
-      // simple substring check should work fine.
-      const index = option.indexOf(this.argument);
-
-      if (index === -1) {
-        continue;
-      }
-
-      if (result.length === 10) {
-        break;
-      }
-
-      result.push(
-        m(
-          'div',
-          {
-            onclick: () => {
-              this.setArgument(attrs, option);
-            },
-          },
-          option.substring(0, index),
-          // Highlight the matching part with bold font
-          m('strong', this.argument),
-          option.substring(index + this.argument.length),
-        ),
-      );
-    }
-
-    return result;
-  }
-
   view({attrs}: m.Vnode<ArgumentPopupArgs>): m.Child {
     return m(
       '.name-completion',
@@ -94,8 +44,6 @@
         },
         value: this.argument,
       }),
-      m('.arguments-popup-sizer', longestString(attrs.knownArguments)),
-      this.renderMatches(attrs),
     );
   }
 }
diff --git a/ui/src/frontend/plugins_page.ts b/ui/src/frontend/plugins_page.ts
index 19c0739..ed2b2f5 100644
--- a/ui/src/frontend/plugins_page.ts
+++ b/ui/src/frontend/plugins_page.ts
@@ -22,6 +22,7 @@
 import {PluginDescriptor} from '../public';
 import {createPage} from './pages';
 import {defaultPlugins} from '../core/default_plugins';
+import {Intent} from '../widgets/common';
 
 export const PluginsPage = createPage({
   view() {
@@ -31,7 +32,7 @@
       m(
         '.pf-plugins-topbar',
         m(Button, {
-          minimal: false,
+          intent: Intent.Primary,
           label: 'Disable All',
           onclick: async () => {
             for (const plugin of pluginRegistry.values()) {
@@ -41,7 +42,7 @@
           },
         }),
         m(Button, {
-          minimal: false,
+          intent: Intent.Primary,
           label: 'Enable All',
           onclick: async () => {
             for (const plugin of pluginRegistry.values()) {
@@ -51,7 +52,7 @@
           },
         }),
         m(Button, {
-          minimal: false,
+          intent: Intent.Primary,
           label: 'Restore Defaults',
           onclick: async () => {
             await pluginManager.restoreDefaults(true);
@@ -95,6 +96,7 @@
       : m('.pf-tag.pf-inactive', 'Inactive'),
     m(Button, {
       label: isActive ? 'Disable' : 'Enable',
+      intent: Intent.Primary,
       onclick: async () => {
         if (isActive) {
           await pluginManager.disablePlugin(pluginId, true);
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index 7a7addc..8220b44 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -35,7 +35,7 @@
 
 // Returns whether incoming traces should be opened automatically or should
 // instead require a user interaction.
-function isTrustedOrigin(origin: string): boolean {
+export function isTrustedOrigin(origin: string): boolean {
   const TRUSTED_ORIGINS = [
     'https://chrometto.googleplex.com',
     'https://uma.googleplex.com',
@@ -46,8 +46,15 @@
   if (isUserTrustedOrigin(origin)) return true;
 
   const hostname = new URL(origin).hostname;
-  if (hostname.endsWith('corp.google.com')) return true;
-  if (hostname === 'localhost' || hostname === '127.0.0.1') return true;
+  if (hostname.endsWith('.corp.google.com')) return true;
+  if (hostname.endsWith('.c.googlers.com')) return true;
+  if (
+    hostname === 'localhost' ||
+    hostname === '127.0.0.1' ||
+    hostname === '[::1]'
+  ) {
+    return true;
+  }
   return false;
 }
 
diff --git a/ui/src/frontend/post_message_handler_unittest.ts b/ui/src/frontend/post_message_handler_unittest.ts
new file mode 100644
index 0000000..c18aff6
--- /dev/null
+++ b/ui/src/frontend/post_message_handler_unittest.ts
@@ -0,0 +1,53 @@
+// 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 {isTrustedOrigin} from './post_message_handler';
+
+describe('postMessageHandler', () => {
+  test('baked-in trusted origins are trusted', () => {
+    expect(isTrustedOrigin('https://chrometto.googleplex.com')).toBeTruthy();
+    expect(isTrustedOrigin('https://uma.googleplex.com')).toBeTruthy();
+    expect(
+      isTrustedOrigin('https://android-build.googleplex.com'),
+    ).toBeTruthy();
+    expect(isTrustedOrigin('https://html5zombo.com')).toBeFalsy();
+  });
+
+  test('user trusted origins in local storage are trusted', () => {
+    try {
+      expect(isTrustedOrigin('https://html5zombo.com')).toBeFalsy();
+      window.localStorage['trustedOrigins'] = '["https://html5zombo.com"]';
+      expect(isTrustedOrigin('https://html5zombo.com')).toBeTruthy();
+    } finally {
+      window.localStorage.clear();
+    }
+  });
+
+  test('developer hostnames are trusted', () => {
+    expect(isTrustedOrigin('https://google.com')).toBeFalsy();
+    expect(isTrustedOrigin('https://broccoliman.corp.google.com')).toBeTruthy();
+    expect(isTrustedOrigin('http://broccoliman.corp.google.com')).toBeTruthy();
+    expect(isTrustedOrigin('https://broccoliman.c.googlers.com')).toBeTruthy();
+    expect(isTrustedOrigin('http://broccoliman.c.googlers.com')).toBeTruthy();
+    expect(isTrustedOrigin('https://broccolimancorp.google.com')).toBeFalsy();
+    expect(isTrustedOrigin('https://broccolimanc.googlers.com')).toBeFalsy();
+    expect(isTrustedOrigin('https://localhost')).toBeTruthy();
+    expect(isTrustedOrigin('http://localhost')).toBeTruthy();
+    expect(isTrustedOrigin('https://127.0.0.1')).toBeTruthy();
+    expect(isTrustedOrigin('http://127.0.0.1')).toBeTruthy();
+    // IPv6 localhost
+    expect(isTrustedOrigin('https://[::1]')).toBeTruthy();
+    expect(isTrustedOrigin('http://[::1]')).toBeTruthy();
+  });
+});
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index f7fbf62..23b5148 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -130,7 +130,7 @@
           : m(
               PopupMenu2,
               {
-                trigger: m(Button, {label: 'Show debug track', minimal: true}),
+                trigger: m(Button, {label: 'Show debug track'}),
                 popupPosition: PopupPosition.Top,
               },
               m(AddDebugTrackMenu, {
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index 4affa45..e9bbce9 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -249,7 +249,6 @@
       contextButtons,
       m(Button, {
         label: 'Copy query',
-        minimal: true,
         onclick: () => {
           copyToClipboard(query);
         },
@@ -258,7 +257,6 @@
         resp.error === undefined &&
         m(Button, {
           label: 'Copy result (.tsv)',
-          minimal: true,
           onclick: () => {
             queryResponseToClipboard(resp);
           },
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index d9e3f0d..3fcc178 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -13,8 +13,10 @@
 // limitations under the License.
 
 import {searchSegment} from '../base/binary_search';
+import {assertUnreachable} from '../base/logging';
 import {Actions} from '../common/actions';
 import {globals} from './globals';
+import {verticalScrollToTrack} from './scroll_helper';
 
 function setToPrevious(current: number) {
   let index = current - 1;
@@ -34,7 +36,7 @@
   const vizWindow = globals.stateVisibleTime();
   const startNs = vizWindow.start;
   const endNs = vizWindow.end;
-  const currentTs = globals.currentSearchResults.tsStarts[index];
+  const currentTs = globals.currentSearchResults.tses[index];
 
   // If the value of |globals.currentSearchResults.totalResults| is 0,
   // it means that the query is in progress or no results are found.
@@ -44,12 +46,12 @@
 
   // If this is a new search or the currentTs is not in the viewport,
   // select the first/last item in the viewport.
-  if (index === -1 || currentTs < startNs || currentTs > endNs) {
+  if (
+    index === -1 ||
+    (currentTs !== -1n && (currentTs < startNs || currentTs > endNs))
+  ) {
     if (reverse) {
-      const [smaller] = searchSegment(
-        globals.currentSearchResults.tsStarts,
-        endNs,
-      );
+      const [smaller] = searchSegment(globals.currentSearchResults.tses, endNs);
       // If there is no item in the viewport just go to the previous.
       if (smaller === -1) {
         setToPrevious(index);
@@ -58,7 +60,7 @@
       }
     } else {
       const [, larger] = searchSegment(
-        globals.currentSearchResults.tsStarts,
+        globals.currentSearchResults.tses,
         startNs,
       );
       // If there is no item in the viewport just go to the next.
@@ -82,52 +84,61 @@
 function selectCurrentSearchResult() {
   const searchIndex = globals.state.searchIndex;
   const source = globals.currentSearchResults.sources[searchIndex];
-  const currentId = globals.currentSearchResults.sliceIds[searchIndex];
+  const currentId = globals.currentSearchResults.eventIds[searchIndex];
   const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
 
   if (currentId === undefined) return;
 
-  if (source === 'cpu') {
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id: currentId,
-        trackKey,
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-  } else if (source === 'log') {
-    globals.setLegacySelection(
-      {
-        kind: 'LOG',
-        id: currentId,
-        trackKey,
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-  } else {
-    // Search results only include slices from the slice table for now.
-    // When we include annotations we need to pass the correct table.
-    globals.setLegacySelection(
-      {
-        kind: 'CHROME_SLICE',
-        id: currentId,
-        trackKey,
-        table: 'slice',
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
+  switch (source) {
+    case 'track':
+      verticalScrollToTrack(trackKey, true);
+      break;
+    case 'cpu':
+      globals.setLegacySelection(
+        {
+          kind: 'SLICE',
+          id: currentId,
+          trackKey,
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    case 'log':
+      globals.setLegacySelection(
+        {
+          kind: 'LOG',
+          id: currentId,
+          trackKey,
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    case 'slice':
+      // Search results only include slices from the slice table for now.
+      // When we include annotations we need to pass the correct table.
+      globals.setLegacySelection(
+        {
+          kind: 'CHROME_SLICE',
+          id: currentId,
+          trackKey,
+          table: 'slice',
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    default:
+      assertUnreachable(source);
   }
 }
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 084c14f..361480b 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -17,6 +17,7 @@
 import {BaseCounterTrack, CounterOptions} from './base_counter_track';
 import {CounterColumns, SqlDataSource} from './debug_tracks';
 import {Disposable, DisposableCallback} from '../base/disposable';
+import {uuidv4Sql} from '../base/uuid';
 
 export type SimpleCounterTrackConfig = {
   data: SqlDataSource;
@@ -39,7 +40,7 @@
       options: config.options,
     });
     this.config = config;
-    this.sqlTableName = `__simple_counter_${this.trackKey}`;
+    this.sqlTableName = `__simple_counter_${uuidv4Sql()}`;
   }
 
   async onInit(): Promise<Disposable> {
@@ -74,7 +75,7 @@
 
   private async dropTrackTable(): Promise<void> {
     if (this.engine.isAlive) {
-      this.engine.query(`drop table if exists ${this.sqlTableName}`);
+      await this.engine.query(`drop table if exists ${this.sqlTableName}`);
     }
   }
 }
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index baa907d..6d287b3 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -87,13 +87,12 @@
     // TODO(altimin): Support removing this table when the track is closed.
     const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
     await this.engine.query(`
-      create table ${this.sqlTableName} as
+      create perfetto table ${this.sqlTableName} as
       with data${dataColumns} as (
         ${data.sqlSource}
       ),
       prepared_data as (
         select
-          row_number() over () as id,
           ${sliceColumns.ts} as ts,
           ifnull(cast(${dur} as int), -1) as dur,
           printf('%s', ${sliceColumns.name}) as name
@@ -102,6 +101,7 @@
         from data
       )
       select
+        row_number() over (order by ts) as id,
         *
       from prepared_data
       order by ts;`);
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index 01c87af..ea173a0 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -94,13 +94,11 @@
         icon: Icons.GoBack,
         disabled: !this.state.canGoBack(),
         onclick: () => this.state.goBack(),
-        minimal: true,
       }),
       m(Button, {
         icon: Icons.GoForward,
         disabled: !this.state.canGoForward(),
         onclick: () => this.state.goForward(),
-        minimal: true,
       }),
     ];
     const {selectStatement, columns} = this.state.buildSqlSelectStatement();
diff --git a/ui/src/frontend/tables/attribute_modal_holder.ts b/ui/src/frontend/tables/attribute_modal_holder.ts
index 562a922..d154ab1 100644
--- a/ui/src/frontend/tables/attribute_modal_holder.ts
+++ b/ui/src/frontend/tables/attribute_modal_holder.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 
 import {showModal} from '../../widgets/modal';
-import {globals} from '../globals';
 import {ArgumentPopup} from '../pivot_table_argument_popup';
 
 export class AttributeModalHolder {
@@ -45,8 +44,6 @@
 
   private renderModalContents() {
     return m(ArgumentPopup, {
-      knownArguments:
-        globals.state.nonSerializableState.pivotTable.argumentNames,
       onArgumentChange: (arg) => {
         this.typedArgument = arg;
       },
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 26dcb57..fac6b8a 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -24,6 +24,7 @@
 import {Section} from '../widgets/section';
 import {SqlRef} from '../widgets/sql_ref';
 import {Tree, TreeNode} from '../widgets/tree';
+import {Intent} from '../widgets/common';
 
 import {BottomTab, NewBottomTabArgs} from './bottom_tab';
 import {SchedSqlId, ThreadStateSqlId} from './sql_types';
@@ -319,6 +320,7 @@
       ),
       m(Button, {
         label: 'Critical path lite',
+        intent: Intent.Primary,
         onclick: () =>
           runQuery(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
@@ -355,6 +357,7 @@
       }),
       m(Button, {
         label: 'Critical path',
+        intent: Intent.Primary,
         onclick: () =>
           runQuery(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 2703720..90f3e4c 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -84,19 +84,21 @@
       );
     }
     const index = globals.state.searchIndex;
-    if (index !== -1 && index < globals.currentSearchResults.tsStarts.length) {
-      const start = globals.currentSearchResults.tsStarts[index];
-      const triangleStart =
-        Math.max(visibleTimeScale.timeToPx(Time.fromRaw(start)), 0) +
-        TRACK_SHELL_WIDTH;
-      ctx.fillStyle = '#000';
-      ctx.beginPath();
-      ctx.moveTo(triangleStart, size.height);
-      ctx.lineTo(triangleStart - 3, 0);
-      ctx.lineTo(triangleStart + 3, 0);
-      ctx.lineTo(triangleStart, size.height);
-      ctx.fill();
-      ctx.closePath();
+    if (index !== -1 && index < globals.currentSearchResults.tses.length) {
+      const start = globals.currentSearchResults.tses[index];
+      if (start !== -1n) {
+        const triangleStart =
+          Math.max(visibleTimeScale.timeToPx(Time.fromRaw(start)), 0) +
+          TRACK_SHELL_WIDTH;
+        ctx.fillStyle = '#000';
+        ctx.beginPath();
+        ctx.moveTo(triangleStart, size.height);
+        ctx.lineTo(triangleStart - 3, 0);
+        ctx.lineTo(triangleStart + 3, 0);
+        ctx.lineTo(triangleStart, size.height);
+        ctx.fill();
+        ctx.closePath();
+      }
     }
 
     ctx.restore();
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 1e35329..ec9a43a 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -150,7 +150,6 @@
                 e.stopPropagation();
               },
               icon: checkBox,
-              minimal: true,
               compact: true,
             }),
         ),
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 59c9857..806b49c 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -39,6 +39,7 @@
 import {getLegacySelection} from '../common/state';
 import {CloseTrackButton} from './close_track_button';
 import {exists} from '../base/utils';
+import {Intent} from '../widgets/common';
 
 function getTitleSize(title: string): string | undefined {
   const length = title.length;
@@ -99,7 +100,7 @@
       {
         trigger: m(Button, {
           icon: Icons.Crashed,
-          minimal: true,
+          compact: true,
         }),
       },
       this.renderErrorMessage(attrs.error),
@@ -112,6 +113,7 @@
       'This track has crashed',
       m(Button, {
         label: 'Re-raise exception',
+        intent: Intent.Primary,
         className: Popup.DISMISS_POPUP_GROUP_CLASS,
         onclick: () => {
           throw error;
@@ -190,7 +192,6 @@
             icon: Icons.Pin,
             iconFilled: pinned,
             title: pinned ? 'Unpin' : 'Pin to top',
-            minimal: true,
             compact: true,
           }),
           currentSelection !== null && currentSelection.kind === 'AREA'
@@ -204,7 +205,6 @@
                   );
                   e.stopPropagation();
                 },
-                minimal: true,
                 compact: true,
                 icon: isSelected(attrs.trackKey)
                   ? Icons.Checkbox
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 73decc9..b599dc7 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -293,7 +293,7 @@
           },
         },
         m(
-          '.header',
+          '.pf-timeline-header',
           m(PanelContainer, {
             className: 'header-panel-container',
             panels: [
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index 2792b7b..65fb04e 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -49,6 +49,12 @@
 import {PopupMenuButton} from './popup_menu';
 import {TableShowcase} from './tables/table_showcase';
 import {TreeTable, TreeTableAttrs} from './widgets/treetable';
+import {Intent} from '../widgets/common';
+import {
+  VirtualTable,
+  VirtualTableAttrs,
+  VirtualTableRow,
+} from '../widgets/virtual_table';
 
 const DATA_ENGLISH_LETTER_FREQUENCY = {
   table: [
@@ -290,6 +296,7 @@
       return [
         m(Button, {
           label: 'Toggle Portal',
+          intent: Intent.Primary,
           onclick: () => {
             portalOpen = !portalOpen;
             raf.scheduleFullRedraw();
@@ -567,6 +574,11 @@
   },
 ];
 
+let virtualTableData: {offset: number; rows: VirtualTableRow[]} = {
+  offset: 0,
+  rows: [],
+};
+
 export const WidgetsPage = createPage({
   view() {
     return m(
@@ -586,7 +598,7 @@
           icon: true,
           rightIcon: false,
           disabled: false,
-          minimal: false,
+          intent: new EnumOption(Intent.None, Object.values(Intent)),
           active: false,
           compact: false,
           loading: false,
@@ -1152,10 +1164,38 @@
           return m(TreeTable<File>, attrs);
         },
       }),
+      m(WidgetShowcase, {
+        label: 'VirtualTable',
+        description: `Virtualized table for efficient rendering of large datasets`,
+        renderWidget: () => {
+          const attrs: VirtualTableAttrs = {
+            columns: [
+              {header: 'x', width: '4em'},
+              {header: 'x^2', width: '8em'},
+            ],
+            rows: virtualTableData.rows,
+            firstRowOffset: virtualTableData.offset,
+            rowHeight: 20,
+            numRows: 500_000,
+            style: {height: '200px'},
+            onReload: (rowOffset, rowCount) => {
+              const rows = [];
+              for (let i = rowOffset; i < rowOffset + rowCount; i++) {
+                rows.push({id: i, cells: [i, i ** 2]});
+              }
+              virtualTableData = {
+                offset: rowOffset,
+                rows,
+              };
+              raf.scheduleFullRedraw();
+            },
+          };
+          return m(VirtualTable, attrs);
+        },
+      }),
     );
   },
 });
-
 class ModalShowcase implements m.ClassComponent {
   private static counter = 0;
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 5fb1a1a..166a007 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -64,15 +64,17 @@
       sf_callback_missed_frames,
       hwui_callback_missed_frames,
       cuj_layer.layer_name,
-      cuj.ts,
-      cuj.dur,
+      /* Boundaries table doesn't contain ts and dur when a CUJ didn't complete successfully.
+        In that case we still want to show that it was canceled, so let's take the slice timestamps. */
+      CASE WHEN boundaries.ts IS NOT NULL THEN boundaries.ts ELSE cuj.ts END AS ts,
+      CASE WHEN boundaries.dur IS NOT NULL THEN boundaries.dur ELSE cuj.dur END AS dur,
       cuj.track_id,
       cuj.slice_id
     FROM slice AS cuj
-           JOIN process_track AS pt
-                ON cuj.track_id = pt.id
+           JOIN process_track AS pt ON cuj.track_id = pt.id
            LEFT JOIN android_jank_cuj jc
                      ON pt.upid = jc.upid AND cuj.name = jc.cuj_slice_name AND cuj.ts = jc.ts
+           LEFT JOIN android_jank_cuj_main_thread_cuj_boundary boundaries using (cuj_id)
            LEFT JOIN android_jank_cuj_layer_name cuj_layer USING (cuj_id)
            LEFT JOIN android_jank_cuj_counter_metrics USING (cuj_id)
     WHERE cuj.name GLOB 'J<*>'
@@ -129,7 +131,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
-      name: 'Pin: Android Jank CUJs',
+      name: 'Add track: Android jank CUJs',
       callback: () => {
         runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() => {
           addDebugSliceTrack(
@@ -140,7 +142,7 @@
             },
             'Jank CUJs',
             {ts: 'ts', dur: 'dur', name: 'name'},
-            [],
+            JANK_COLUMNS,
           );
         });
       },
@@ -148,7 +150,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
-      name: 'Run query: Android Jank CUJs',
+      name: 'Run query: Android jank CUJs',
       callback: () => {
         runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
           ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'),
@@ -158,7 +160,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs',
-      name: 'Pin: Android Latency CUJs',
+      name: 'Add track: Android latency CUJs',
       callback: () => {
         addDebugSliceTrack(
           ctx.engine,
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index 0eb7e0f..9cb8405 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -41,7 +41,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#batteryEvents',
-      name: 'Run query: Pin battery events',
+      name: 'Add track: battery events',
       callback: async (track) => {
         if (track === undefined) {
           track = prompt('Battery Track', '');
@@ -62,7 +62,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#activityTrack',
-      name: 'Run query: Visualize Network Activity',
+      name: 'Add track: network activity',
       callback: async (groupby, filter, trackName) => {
         if (groupby === undefined) {
           groupby = prompt('Group by', 'package_name');
diff --git a/ui/src/plugins/dev.perfetto.BookmarkletApi/OWNERS b/ui/src/plugins/dev.perfetto.BookmarkletApi/OWNERS
index 888f192..e159207 100644
--- a/ui/src/plugins/dev.perfetto.BookmarkletApi/OWNERS
+++ b/ui/src/plugins/dev.perfetto.BookmarkletApi/OWNERS
@@ -1,2 +1 @@
-hjd@google.com
 stevegolton@google.com
diff --git a/ui/src/plugins/dev.perfetto.Chaos/OWNERS b/ui/src/plugins/dev.perfetto.Chaos/OWNERS
index 9ee9fce..e159207 100644
--- a/ui/src/plugins/dev.perfetto.Chaos/OWNERS
+++ b/ui/src/plugins/dev.perfetto.Chaos/OWNERS
@@ -1 +1 @@
-hjd@google.com
+stevegolton@google.com
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
index 8dc6a6f..54cd458 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
@@ -19,7 +19,6 @@
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
-  addDebugSliceTrack,
 } from '../../public';
 
 const SQL_STATS = `
@@ -107,7 +106,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#RunQueryAllProcesses',
-      name: 'Run query: all processes',
+      name: 'Run query: All processes',
       callback: () => {
         ctx.tabs.openQuery(ALL_PROCESSES_QUERY, 'All Processes');
       },
@@ -138,7 +137,7 @@
       callback: () => {
         ctx.tabs.openQuery(
           CPU_TIME_BY_CPU_BY_PROCESS,
-          'CPU Time by CPU by process',
+          'CPU time by CPU by process',
         );
       },
     });
@@ -163,18 +162,8 @@
     });
 
     ctx.registerCommand({
-      id: 'dev.perfetto.CoreCommands#PinFtraceTracks',
-      name: 'Pin ftrace tracks',
-      callback: () => {
-        ctx.timeline.pinTracksByPredicate((tags) => {
-          return !!tags.name?.startsWith('Ftrace Events Cpu ');
-        });
-      },
-    });
-
-    ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#UnpinAllTracks',
-      name: 'Unpin all tracks',
+      name: 'Unpin all pinned tracks',
       callback: () => {
         ctx.timeline.unpinTracksByPredicate((_) => {
           return true;
@@ -184,7 +173,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#ExpandAllGroups',
-      name: 'Expand all groups',
+      name: 'Expand all track groups',
       callback: () => {
         ctx.timeline.expandGroupsByPredicate((_) => {
           return true;
@@ -194,7 +183,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#CollapseAllGroups',
-      name: 'Collapse all groups',
+      name: 'Collapse all track groups',
       callback: () => {
         ctx.timeline.collapseGroupsByPredicate((_) => {
           return true;
@@ -204,7 +193,7 @@
 
     ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#PanToTimestamp',
-      name: 'Pan To Timestamp',
+      name: 'Pan to timestamp',
       callback: (tsRaw: unknown) => {
         if (exists(tsRaw)) {
           if (typeof tsRaw !== 'bigint') {
@@ -222,29 +211,8 @@
     });
 
     ctx.registerCommand({
-      id: 'test',
-      name: 'Make Test Debug Track',
-      callback: () => {
-        addDebugSliceTrack(
-          ctx.engine,
-          {
-            sqlSource: `
-              SELECT *
-              FROM slice
-              WHERE name like 'a%'
-              LIMIT 10000
-            `,
-          },
-          'Track Name',
-          {ts: 'ts', dur: 'dur', name: 'name'},
-          [],
-        );
-      },
-    });
-
-    ctx.registerCommand({
       id: 'dev.perfetto.CoreCommands#ShowCurrentSelectionTab',
-      name: 'Show Current Selection Tab',
+      name: 'Show current selection tab',
       callback: () => {
         ctx.tabs.showTab('current_selection');
       },
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index d7a37d8..b9127f0 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -27,23 +27,11 @@
 class LinuxKernelDevices implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
-      with
-        slices_tracks as materialized (
-          select distinct track_id
-          from slice
-        ),
-        tracks as (
-          select
-            linux_device_track.id as track_id,
-            linux_device_track.name
-          from linux_device_track
-          join slices_tracks on
-          slices_tracks.track_id = linux_device_track.id
-        )
       select
-        t.name,
-        t.track_id as trackId
-      from tracks as t
+        t.id as trackId,
+        t.name
+      from linux_device_track t
+      join _slice_track_summary using (id)
       order by t.name;
     `);
 
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index e51d5d5..ece6ea0 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -45,9 +45,9 @@
   // These properties are updated only once per query result when the Slice
   // object is created and don't change afterwards.
   readonly id: number;
-  readonly startNsQ: time;
-  readonly endNsQ: time;
-  readonly durNsQ: duration;
+  readonly startNs: time;
+  readonly endNs: time;
+  readonly durNs: duration;
   readonly ts: time;
   readonly dur: duration;
   readonly depth: number;
@@ -177,10 +177,15 @@
 
 export interface Track {
   /**
-   * Optional: Called when the track is first materialized on the timeline.
+   * Optional: Called once before onUpdate is first called.
+   *
    * If this function returns a Promise, this promise is awaited before onUpdate
    * or onDestroy is called. Any calls made to these functions in the meantime
    * will be queued up and the hook will be called later once onCreate returns.
+   *
+   * Exactly when this hook is called is left purposely undefined. The only
+   * guarantee is that it will be called once before onUpdate is first called.
+   *
    * @param ctx Our track context object.
    */
   onCreate?(ctx: TrackContext): Promise<void> | void;
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 60c4487..f5d3d7a 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -22,7 +22,7 @@
 export function getTrackName(
   args: Partial<{
     name: string | null;
-    utid: number;
+    utid: number | null;
     processName: string | null;
     pid: number | null;
     threadName: string | null;
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index c35143e..53e6f69 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -15,11 +15,7 @@
 import m from 'mithril';
 
 import {LogFilteringCriteria, LogPanel} from './logs_panel';
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-} from '../../public';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {NUM} from '../../trace_processor/query_result';
 import {AndroidLogTrack} from './logs_track';
 
@@ -88,7 +84,7 @@
 
     ctx.registerCommand({
       id: 'perfetto.AndroidLog#ShowLogsTab',
-      name: 'Show Android Logs Tab',
+      name: 'Show android logs tab',
       callback: () => {
         ctx.tabs.showTab(androidLogsTabUri);
       },
diff --git a/ui/src/tracks/android_log/logs_panel.ts b/ui/src/tracks/android_log/logs_panel.ts
index 1a8d233..3970190 100644
--- a/ui/src/tracks/android_log/logs_panel.ts
+++ b/ui/src/tracks/android_log/logs_panel.ts
@@ -18,18 +18,19 @@
 import {Actions} from '../../common/actions';
 import {raf} from '../../core/raf_scheduler';
 import {DetailsShell} from '../../widgets/details_shell';
-import {VirtualScrollContainer} from '../../widgets/virtual_scroll_container';
 
-import {SELECTED_LOG_ROWS_COLOR} from '../../frontend/css_constants';
 import {globals} from '../../frontend/globals';
 import {Timestamp} from '../../frontend/widgets/timestamp';
-import {createStore, EngineProxy, LONG, NUM, Store, STR} from '../../public';
+import {EngineProxy, LONG, NUM, NUM_NULL, Store, STR} from '../../public';
 import {Monitor} from '../../base/monitor';
 import {AsyncLimiter} from '../../base/async_limiter';
 import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
 import {Select} from '../../widgets/select';
 import {Button} from '../../widgets/button';
 import {TextInput} from '../../widgets/text_input';
+import {Intent} from '../../widgets/common';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
+import {classNames} from '../../base/classnames';
 
 const ROW_H = 20;
 
@@ -62,15 +63,12 @@
 }
 
 export class LogPanel implements m.ClassComponent<LogPanelAttrs> {
-  private readonly SKIRT_SIZE = 50;
   private entries?: LogEntries;
-  private isStale = true;
-  private viewportBounds = {top: 0, bottom: 0};
 
-  private readonly paginationStore = createStore<Pagination>({
+  private pagination: Pagination = {
     offset: 0,
     count: 0,
-  });
+  };
   private readonly rowsMonitor: Monitor;
   private readonly filterMonitor: Monitor;
   private readonly queryLimiter = new AsyncLimiter();
@@ -80,7 +78,6 @@
       () => attrs.filterStore.state,
       () => globals.state.frontendLocalState.visibleState.start,
       () => globals.state.frontendLocalState.visibleState.end,
-      () => this.paginationStore.state,
     ]);
 
     this.filterMonitor = new Monitor([() => attrs.filterStore.state]);
@@ -88,148 +85,104 @@
 
   view({attrs}: m.CVnode<LogPanelAttrs>) {
     if (this.rowsMonitor.ifStateChanged()) {
-      this.queryLimiter.schedule(async () => {
-        this.isStale = true;
-        raf.scheduleFullRedraw();
-
-        const visibleState = globals.state.frontendLocalState.visibleState;
-        const visibleSpan = new TimeSpan(visibleState.start, visibleState.end);
-
-        if (this.filterMonitor.ifStateChanged()) {
-          await updateLogView(attrs.engine, attrs.filterStore.state);
-        }
-
-        this.entries = await updateLogEntries(
-          attrs.engine,
-          visibleSpan,
-          this.paginationStore.state,
-        );
-
-        raf.scheduleFullRedraw();
-        this.isStale = false;
-      });
+      this.reloadData(attrs);
     }
 
     const hasProcessNames =
       this.entries &&
       this.entries.processName.filter((name) => name).length > 0;
+    const totalEvents = this.entries?.totalEvents ?? 0;
 
-    const rows: m.Children = [];
-    rows.push(
-      m(
-        `.row`,
-        m('.cell.row-header', 'Timestamp'),
-        m('.cell.row-header', 'Level'),
-        m('.cell.row-header', 'Tag'),
-        hasProcessNames
-          ? m('.cell.with-process.row-header', 'Process name')
-          : undefined,
-        hasProcessNames
-          ? m('.cell.with-process.row-header', 'Message')
-          : m('.cell.no-process.row-header', 'Message'),
-        m('br'),
-      ),
-    );
-    if (this.entries) {
-      const offset = this.entries.offset;
-      const timestamps = this.entries.timestamps;
-      const priorities = this.entries.priorities;
-      const tags = this.entries.tags;
-      const messages = this.entries.messages;
-      const processNames = this.entries.processName;
-      const totalEvents = this.entries.totalEvents;
-
-      for (let i = 0; i < this.entries.timestamps.length; i++) {
-        const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
-        const ts = timestamps[i];
-        const prioClass = priorityLetter || '';
-        const style: {top: string; backgroundColor?: string} = {
-          // 1.5 is for the width of the header
-          top: `${(offset + i + 1.5) * ROW_H}px`,
-        };
-        if (this.entries.isHighlighted[i]) {
-          style.backgroundColor = SELECTED_LOG_ROWS_COLOR;
-        }
-
-        rows.push(
-          m(
-            `.row.${prioClass}`,
-            {
-              class: this.isStale ? 'stale' : '',
-              style,
-              onmouseover: () => {
-                globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
-              },
-              onmouseout: () => {
-                globals.dispatch(
-                  Actions.setHoverCursorTimestamp({ts: Time.INVALID}),
-                );
-              },
-            },
-            m('.cell', m(Timestamp, {ts})),
-            m('.cell', priorityLetter || '?'),
-            m('.cell', tags[i]),
-            hasProcessNames
-              ? m('.cell.with-process', processNames[i])
-              : undefined,
-            hasProcessNames
-              ? m('.cell.with-process', messages[i])
-              : m('.cell.no-process', messages[i]),
-            m('br'),
-          ),
-        );
-      }
-
-      return m(
-        DetailsShell,
-        {
-          title: 'Android Logs',
-          description: `[${this.viewportBounds.top}, ${this.viewportBounds.bottom}] / ${totalEvents}`,
-          buttons: m(LogsFilters, {store: attrs.filterStore}),
+    return m(
+      DetailsShell,
+      {
+        title: 'Android Logs',
+        description: `Total messages: ${totalEvents}`,
+        buttons: m(LogsFilters, {store: attrs.filterStore}),
+      },
+      m(VirtualTable, {
+        className: 'pf-android-logs-table',
+        columns: [
+          {header: 'Timestamp', width: '7rem'},
+          {header: 'Level', width: '4rem'},
+          {header: 'Tag', width: '13rem'},
+          ...(hasProcessNames ? [{header: 'Process', width: '18rem'}] : []),
+          {header: 'Message', width: '42rem'},
+        ],
+        rows: this.renderRows(hasProcessNames),
+        firstRowOffset: this.entries?.offset ?? 0,
+        numRows: this.entries?.totalEvents ?? 0,
+        rowHeight: ROW_H,
+        onReload: (offset, count) => {
+          this.pagination = {offset, count};
+          this.reloadData(attrs);
         },
-        m(
-          VirtualScrollContainer,
-          {
-            onScroll: (scrollContainer: HTMLElement) => {
-              this.recomputeVisibleRowsAndUpdate(scrollContainer);
-              raf.scheduleFullRedraw();
-            },
-          },
-          m(
-            '.log-panel',
-            m('.rows', {style: {height: `${totalEvents * ROW_H}px`}}, rows),
-          ),
-        ),
-      );
-    }
-
-    return null;
+        onRowHover: (id) => {
+          const timestamp = this.entries?.timestamps[id];
+          if (timestamp !== undefined) {
+            globals.dispatch(Actions.setHoverCursorTimestamp({ts: timestamp}));
+          }
+        },
+        onRowOut: () => {
+          globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
+        },
+      }),
+    );
   }
 
-  recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
-    const viewportTop = Math.floor(scrollContainer.scrollTop / ROW_H);
-    const viewportHeight = Math.ceil(scrollContainer.clientHeight / ROW_H);
-    const viewportBottom = viewportTop + viewportHeight;
+  private reloadData(attrs: LogPanelAttrs) {
+    this.queryLimiter.schedule(async () => {
+      const visibleState = globals.state.frontendLocalState.visibleState;
+      const visibleSpan = new TimeSpan(visibleState.start, visibleState.end);
 
-    this.viewportBounds = {
-      top: viewportTop,
-      bottom: viewportBottom,
-    };
+      if (this.filterMonitor.ifStateChanged()) {
+        await updateLogView(attrs.engine, attrs.filterStore.state);
+      }
 
-    const curPage = this.paginationStore.state;
+      this.entries = await updateLogEntries(
+        attrs.engine,
+        visibleSpan,
+        this.pagination,
+      );
 
-    if (
-      viewportTop < curPage.offset ||
-      viewportBottom >= curPage.offset + curPage.count
-    ) {
-      this.paginationStore.edit((draft) => {
-        const offset = Math.max(0, viewportTop - this.SKIRT_SIZE);
-        // Make it even so alternating coloured rows line up
-        const offsetEven = Math.floor(offset / 2) * 2;
-        draft.offset = offsetEven;
-        draft.count = viewportHeight + this.SKIRT_SIZE * 2;
+      raf.scheduleFullRedraw();
+    });
+  }
+
+  private renderRows(hasProcessNames: boolean | undefined): VirtualTableRow[] {
+    if (!this.entries) {
+      return [];
+    }
+
+    const timestamps = this.entries.timestamps;
+    const priorities = this.entries.priorities;
+    const tags = this.entries.tags;
+    const messages = this.entries.messages;
+    const processNames = this.entries.processName;
+
+    const rows: VirtualTableRow[] = [];
+    for (let i = 0; i < this.entries.timestamps.length; i++) {
+      const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
+      const ts = timestamps[i];
+      const prioClass = priorityLetter || '';
+
+      rows.push({
+        id: i,
+        className: classNames(
+          prioClass,
+          this.entries.isHighlighted[i] && 'pf-highlighted',
+        ),
+        cells: [
+          m(Timestamp, {ts}),
+          priorityLetter || '?',
+          tags[i],
+          ...(hasProcessNames ? [processNames[i]] : []),
+          messages[i],
+        ],
       });
     }
+
+    return rows;
   }
 }
 
@@ -285,6 +238,7 @@
       label: attrs.name,
       rightIcon: 'close',
       onclick: () => attrs.removeTag(attrs.name),
+      intent: Intent.Primary,
     });
   }
 }
@@ -371,7 +325,6 @@
       icon,
       title: tooltip,
       disabled: attrs.disabled,
-      minimal: true,
       onclick: attrs.onClick,
     });
   }
@@ -459,7 +412,7 @@
     prio: NUM,
     tag: STR,
     msg: STR,
-    isMsgHighlighted: NUM,
+    isMsgHighlighted: NUM_NULL,
     isProcessHighlighted: NUM,
     processName: STR,
   });
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index 2e8b3d5..18883d9 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -30,77 +30,41 @@
   async addGlobalAsyncTracks(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const rawGlobalAsyncTracks = await engine.query(`
-      with tracks_with_slices as materialized (
-        select distinct track_id
-        from slice
-      ),
-      global_tracks as (
-        select
-          track.parent_id as parent_id,
-          track.id as track_id,
-          track.name as name
-        from track
-        join tracks_with_slices on tracks_with_slices.track_id = track.id
-        where
-          track.type = "track"
-          or track.type = "gpu_track"
-          or track.type = "cpu_track"
-      ),
-      global_tracks_grouped as (
+      with global_tracks_grouped as (
         select
           parent_id,
           name,
-          group_concat(track_id) as trackIds,
-          count(track_id) as trackCount
-        from global_tracks track
+          group_concat(id) as trackIds,
+          count() as trackCount
+        from track t
+        join _slice_track_summary using (id)
+        where t.type in ('track', 'gpu_track', 'cpu_track')
         group by parent_id, name
       )
       select
-        t.parent_id as parentId,
-        p.name as parentName,
         t.name as name,
+        t.parent_id as parentId,
         t.trackIds as trackIds,
         __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks_grouped AS t
-      left join track p on (t.parent_id = p.id)
-      order by p.name, t.name;
+      from global_tracks_grouped t
     `);
     const it = rawGlobalAsyncTracks.iter({
       name: STR_NULL,
-      parentName: STR_NULL,
       parentId: NUM_NULL,
       trackIds: STR,
-      maxDepth: NUM_NULL,
+      maxDepth: NUM,
     });
 
-    // let scrollJankRendered = false;
-
     for (; it.valid(); it.next()) {
       const rawName = it.name === null ? undefined : it.name;
-      // const rawParentName = it.parentName === null ? undefined :
-      // it.parentName;
       const displayName = getTrackName({
         name: rawName,
         kind: ASYNC_SLICE_TRACK_KIND,
       });
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      // const parentTrackId = it.parentId;
       const maxDepth = it.maxDepth;
 
-      // If there are no slices in this track, skip it.
-      if (maxDepth === null) {
-        continue;
-      }
-
-      // if (ENABLE_SCROLL_JANK_PLUGIN_V2.get() && !scrollJankRendered &&
-      //     name.includes(INPUT_LATENCY_TRACK)) {
-      //   // This ensures that the scroll jank tracks render above the tracks
-      //   // for GestureScrollUpdate.
-      //   await this.addScrollJankTracks(this.engine);
-      //   scrollJankRendered = true;
-      // }
-
       ctx.registerTrack({
         uri: `perfetto.AsyncSlices#${rawName}.${it.parentId}`,
         displayName,
@@ -115,27 +79,16 @@
 
   async addProcessAsyncSliceTracks(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where
-            process_track.name is null or
-            process_track.name not like "% Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name is null or t.name not glob "* Timeline"
     `);
 
     const it = result.iter({
@@ -144,7 +97,7 @@
       trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
-      maxDepth: NUM_NULL,
+      maxDepth: NUM,
     });
     for (; it.valid(); it.next()) {
       const upid = it.upid;
@@ -155,11 +108,6 @@
       const pid = it.pid;
       const maxDepth = it.maxDepth;
 
-      if (maxDepth === null) {
-        // If there are no slices in this track, skip it.
-        continue;
-      }
-
       const kind = ASYNC_SLICE_TRACK_KIND;
       const displayName = getTrackName({
         name: trackName,
@@ -188,37 +136,20 @@
   async addUserAsyncSliceTracks(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
-      with tracks_with_slices as materialized (
-        select distinct track_id
-        from slice
-      ),
-      global_tracks as (
-        select
-          uid_track.name,
-          uid_track.uid,
-          group_concat(uid_track.id) as trackIds,
-          count(uid_track.id) as trackCount
-        from uid_track
-        join tracks_with_slices
-        where tracks_with_slices.track_id == uid_track.id
-        group by uid_track.uid
-      )
       select
         t.name as name,
         t.uid as uid,
-        package_list.package_name as package_name,
-        t.trackIds as trackIds,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks t
-      join package_list
-      where t.uid = package_list.uid
-      group by t.uid
-      `);
+        package_list.package_name as packageName,
+        t.track_ids as trackIds,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _uid_track_track_summary_by_uid_and_name t
+      join package_list using (uid)
+    `);
 
     const it = result.iter({
       name: STR_NULL,
       uid: NUM_NULL,
-      package_name: STR_NULL,
+      packageName: STR_NULL,
       trackIds: STR,
       maxDepth: NUM_NULL,
     });
@@ -226,7 +157,7 @@
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
-      const userName = it.package_name === null ? undefined : it.package_name;
+      const userName = it.packageName === null ? undefined : it.packageName;
       const uid = it.uid === null ? undefined : it.uid;
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
index 427acc7..faa863b 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/index.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -284,7 +284,7 @@
   onActivate(ctx: PluginContext): void {
     ctx.registerCommand({
       id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
-      name: 'Add Chrome Interactions track',
+      name: 'Add track: Chrome interactions',
       callback: () => addCriticalUserInteractionTrack(),
     });
   }
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index 85a2a70..11cc6d1 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -53,8 +53,8 @@
     });
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy(): Promise<void> {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(EventLatencyTrack.kind);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_delta_graph.ts b/ui/src/tracks/chrome_scroll_jank/scroll_delta_graph.ts
index 0bd3abe..eedf3be 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_delta_graph.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_delta_graph.ts
@@ -16,7 +16,7 @@
 
 import {duration, Time, time} from '../../base/time';
 import {EngineProxy} from '../../trace_processor/engine';
-import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM} from '../../trace_processor/query_result';
 import {VegaView} from '../../widgets/vega_view';
 
 const USER_CATEGORY = 'User';
@@ -27,14 +27,14 @@
   // to denote the color of the data point.
   category: string;
   offset: number;
-  scrollUpdateIds: string;
+  scrollUpdateId: number;
   ts: number;
   delta: number;
 }
 
 export interface ScrollDeltaDetails {
   ts: time;
-  scrollUpdateIds: string;
+  scrollUpdateId: number;
   scrollDelta: number;
   scrollOffset: number;
 }
@@ -54,9 +54,9 @@
 
     SELECT
       ts,
-      IFNULL(scroll_update_id, "") AS scrollUpdateId,
+      IFNULL(scroll_update_id, 0) AS scrollUpdateId,
       delta_y AS deltaY,
-      offset_y AS offsetY
+      relative_offset_y AS offsetY
     FROM chrome_scroll_input_offsets
     WHERE ts >= ${startTs} AND ts <= ${startTs + dur};
   `);
@@ -72,7 +72,7 @@
   for (; it.valid(); it.next()) {
     deltas.push({
       ts: Time.fromRaw(it.ts),
-      scrollUpdateIds: it.scrollUpdateId.toString(),
+      scrollUpdateId: it.scrollUpdateId,
       scrollOffset: it.offsetY,
       scrollDelta: it.deltaY,
     });
@@ -87,30 +87,21 @@
   dur: duration,
 ): Promise<ScrollDeltaDetails[]> {
   const queryResult = await engine.query(`
-    INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
+    INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets
 
-    WITH scroll_update_ids AS (
-      SELECT DISTINCT
-        ts,
-        GROUP_CONCAT(scroll_update_id, ', ')
-          OVER (PARTITION BY ts) AS scroll_update_ids
-      FROM chrome_presented_scroll_offsets
-    )
     SELECT
       ts,
-      IFNULL(scroll_update_ids, "") AS scrollUpdateIds,
+      IFNULL(scroll_update_id, 0) AS scrollUpdateId,
       delta_y AS deltaY,
-      offset_y AS offsetY
+      relative_offset_y AS offsetY
     FROM chrome_presented_scroll_offsets
-    LEFT JOIN scroll_update_ids
-      USING(ts)
     WHERE ts >= ${startTs} AND ts <= ${startTs + dur}
       AND delta_y IS NOT NULL;
   `);
 
   const it = queryResult.iter({
     ts: LONG,
-    scrollUpdateIds: STR,
+    scrollUpdateId: NUM,
     deltaY: NUM,
     offsetY: NUM,
   });
@@ -122,7 +113,7 @@
 
     deltas.push({
       ts: Time.fromRaw(it.ts),
-      scrollUpdateIds: it.scrollUpdateIds,
+      scrollUpdateId: it.scrollUpdateId,
       scrollOffset: offset,
       scrollDelta: it.deltaY,
     });
@@ -242,7 +233,11 @@
         },
         "tooltip": [
           {"field": "delta", "type": "quantitative", "title": "Delta"},
-          {"field": "scrollUpdateIds", "type": "nominal", "title": "Trace Ids"}
+          {
+            "field": "scrollUpdateId",
+            "type": "quantititive",
+            "title": "Trace Id"
+          }
         ]
       }
     }
@@ -262,7 +257,7 @@
     plotData.push({
       category: category,
       ts: Number(delta.ts) / 10e8,
-      scrollUpdateIds: delta.scrollUpdateIds,
+      scrollUpdateId: delta.scrollUpdateId,
       offset: delta.scrollOffset,
       delta: delta.scrollDelta,
     });
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index ba44b9b..fdb5ad1 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -76,8 +76,8 @@
     };
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy(): Promise<void> {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(ScrollJankV3Track.kind);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index 2967b97..8b47569 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -62,8 +62,8 @@
     });
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy(): Promise<void> {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(
       TopLevelScrollTrack.kind,
     );
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 2ac64d6..55773cd 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -133,23 +133,34 @@
 
   onMouseClick({x}: {x: number}): boolean {
     const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x);
+    const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
 
-    const result = this.engine.query(`
-
+    const query = `
       select
         id,
         ts as leftTs,
-        max(ts) OVER (ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) as rightTs
-        from
-        ${this.rootTable}
-        where track_id = ${this.trackId} and ts > ${time} limit 1`);
+        (
+          select ts
+          from ${this.rootTable}
+          where
+            track_id = ${this.trackId}
+            and ts >= ${time}
+          order by ts
+          limit 1
+        ) as rightTs
+      from ${this.rootTable}
+      where
+        track_id = ${this.trackId}
+        and ts < ${time}
+      order by ts DESC
+      limit 1
+    `;
 
-    result.then((result) => {
+    this.engine.query(query).then((result) => {
       const it = result.iter({
         id: NUM,
         leftTs: LONG,
-        rightTs: LONG,
+        rightTs: LONG_NULL,
       });
       if (!it.valid()) {
         return;
@@ -157,7 +168,11 @@
       const trackKey = this.trackKey;
       const id = it.id;
       const leftTs = Time.fromRaw(it.leftTs);
-      const rightTs = Time.fromRaw(it.rightTs);
+
+      // TODO(stevegolton): Don't try to guess times and durations here, make it
+      // obvious to the user that this counter sample has no duration as it's
+      // the last one in the series
+      const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
 
       globals.makeSelection(
         Actions.selectCounter({
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index 52708e9..b5fa5fa 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -12,13 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {searchSegment} from '../../base/binary_search';
 import {assertTrue} from '../../base/logging';
 import {duration, time, Time} from '../../base/time';
-import {calcCachedBucketSize} from '../../common/cache_utils';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 import {colorForCpu} from '../../core/colorizer';
 import {TrackData} from '../../common/track_data';
@@ -33,20 +30,12 @@
   PluginDescriptor,
   Track,
 } from '../../public';
-import {
-  LONG,
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  QueryResult,
-} from '../../trace_processor/query_result';
+import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
 
 export interface Data extends TrackData {
-  maximumValue: number;
-  maxTsEnd: time;
-
   timestamps: BigInt64Array;
   minFreqKHz: Uint32Array;
   maxFreqKHz: Uint32Array;
@@ -58,8 +47,7 @@
   cpu: number;
   freqTrackId: number;
   idleTrackId?: number;
-  maximumValue?: number;
-  minimumValue?: number;
+  maximumValue: number;
 }
 
 // 0.5 Makes the horizontal lines sharp.
@@ -74,79 +62,72 @@
   private hoveredIdle: number | undefined = undefined;
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
 
-  private maxDur: duration = 0n;
-  private maxTsEnd: time = Time.ZERO;
-  private maximumValueSeen = 0;
-  private cachedBucketSize = BIMath.INT64_MAX;
-
-  // This unique ID is just used to create the table names.
-  // In the future we should probably use the track instance ID, but for now we
-  // don't have access to it.
-  private uuid = uuidv4();
-
   private engine: EngineProxy;
   private config: Config;
+  private trackUuid = uuidv4Sql();
 
   constructor(config: Config, engine: EngineProxy) {
     this.config = config;
     this.engine = engine;
   }
 
-  // Returns a valid SQL table name with the given prefix that should be unique
-  // for each track.
-  private tableName(prefix: string) {
-    // Derive table name from, since that is unique for each track.
-    // Track ID can be UUID but '-' is not valid for sql table name.
-    const idSuffix = this.uuid.split('-').join('_');
-    return `${prefix}_${idSuffix}`;
-  }
-
   async onCreate() {
-    await this.createFreqIdleViews();
+    if (this.config.idleTrackId === undefined) {
+      await this.engine.execute(`
+        create view raw_freq_idle_${this.trackUuid} as
+        select ts, dur, value as freqValue, -1 as idleValue
+        from experimental_counter_dur c
+        where track_id = ${this.config.freqTrackId}
+      `);
+    } else {
+      await this.engine.execute(`
+        create view raw_freq_${this.trackUuid} as
+        select ts, dur, value as freqValue
+        from experimental_counter_dur c
+        where track_id = ${this.config.freqTrackId};
 
-    this.maximumValueSeen = await this.queryMaxFrequency();
-    this.maxDur = await this.queryMaxSourceDur();
+        create view raw_idle_${this.trackUuid} as
+        select
+          ts,
+          dur,
+          iif(value = 4294967295, -1, cast(value as int)) as idleValue
+        from experimental_counter_dur c
+        where track_id = ${this.config.idleTrackId};
 
-    const iter = (
-      await this.engine.query(`
-      select max(ts) as maxTs, dur, count(1) as rowCount
-      from ${this.tableName('freq_idle')}
-    `)
-    ).firstRow({maxTs: LONG_NULL, dur: LONG_NULL, rowCount: NUM});
-    if (iter.maxTs === null || iter.dur === null) {
-      // We shoulnd't really hit this because trackDecider shouldn't create
-      // the track in the first place if there are no entries. But could happen
-      // if only one cpu has no cpufreq data.
-      return;
-    }
-    this.maxTsEnd = Time.add(Time.fromRaw(iter.maxTs), iter.dur);
-
-    const rowCount = iter.rowCount;
-    const bucketSize = calcCachedBucketSize(rowCount);
-    if (bucketSize === undefined) {
-      return;
+        create virtual table raw_freq_idle_${this.trackUuid}
+        using span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid});
+      `);
     }
 
-    await this.engine.query(`
-      create table ${this.tableName('freq_idle_cached')} as
-      select
-        (ts + ${bucketSize / 2n}) / ${bucketSize} * ${bucketSize} as cachedTsq,
-        min(freqValue) as minFreq,
-        max(freqValue) as maxFreq,
-        value_at_max_ts(ts, freqValue) as lastFreq,
-        value_at_max_ts(ts, idleValue) as lastIdleValue
-      from ${this.tableName('freq_idle')}
-      group by cachedTsq
-      order by cachedTsq
+    await this.engine.execute(`
+      create virtual table cpu_freq_${this.trackUuid}
+      using __intrinsic_counter_mipmap((
+        select ts, freqValue as value
+        from raw_freq_idle_${this.trackUuid}
+      ));
+
+      create virtual table cpu_idle_${this.trackUuid}
+      using __intrinsic_counter_mipmap((
+        select ts, idleValue as value
+        from raw_freq_idle_${this.trackUuid}
+      ));
     `);
-
-    this.cachedBucketSize = bucketSize;
   }
 
   async onUpdate() {
     await this.fetcher.requestDataForCurrentTime();
   }
 
+  async onDestroy(): Promise<void> {
+    if (this.engine.isAlive) {
+      await this.engine.query(`drop table cpu_freq_${this.trackUuid}`);
+      await this.engine.query(`drop table cpu_idle_${this.trackUuid}`);
+      await this.engine.query(`drop table raw_freq_idle_${this.trackUuid}`);
+      await this.engine.query(`drop view if exists raw_freq_${this.trackUuid}`);
+      await this.engine.query(`drop view if exists raw_idle_${this.trackUuid}`);
+    }
+  }
+
   async onBoundsChange(
     start: time,
     end: time,
@@ -156,164 +137,62 @@
     // function to make sense.
     assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
 
-    const freqResult = await this.queryData(start, end, resolution);
-    assertTrue(freqResult.isComplete());
+    const freqResult = await this.engine.query(`
+      SELECT
+        min_value as minFreq,
+        max_value as maxFreq,
+        last_ts as ts,
+        last_value as lastFreq
+      FROM cpu_freq_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+    const idleResult = await this.engine.query(`
+      SELECT last_value as lastIdle
+      FROM cpu_idle_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
 
-    const numRows = freqResult.numRows();
+    const freqRows = freqResult.numRows();
+    const idleRows = idleResult.numRows();
+    assertTrue(freqRows == idleRows);
+
     const data: Data = {
       start,
       end,
       resolution,
-      length: numRows,
-      maximumValue: this.maximumValue(),
-      maxTsEnd: this.maxTsEnd,
-      timestamps: new BigInt64Array(numRows),
-      minFreqKHz: new Uint32Array(numRows),
-      maxFreqKHz: new Uint32Array(numRows),
-      lastFreqKHz: new Uint32Array(numRows),
-      lastIdleValues: new Int8Array(numRows),
+      length: freqRows,
+      timestamps: new BigInt64Array(freqRows),
+      minFreqKHz: new Uint32Array(freqRows),
+      maxFreqKHz: new Uint32Array(freqRows),
+      lastFreqKHz: new Uint32Array(freqRows),
+      lastIdleValues: new Int8Array(freqRows),
     };
 
-    const it = freqResult.iter({
-      tsq: LONG,
+    const freqIt = freqResult.iter({
+      ts: LONG,
       minFreq: NUM,
       maxFreq: NUM,
       lastFreq: NUM,
-      lastIdleValue: NUM,
     });
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = it.tsq;
-      data.minFreqKHz[i] = it.minFreq;
-      data.maxFreqKHz[i] = it.maxFreq;
-      data.lastFreqKHz[i] = it.lastFreq;
-      data.lastIdleValues[i] = it.lastIdleValue;
+    const idleIt = idleResult.iter({
+      lastIdle: NUM,
+    });
+    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
+      data.timestamps[i] = freqIt.ts;
+      data.minFreqKHz[i] = freqIt.minFreq;
+      data.maxFreqKHz[i] = freqIt.maxFreq;
+      data.lastFreqKHz[i] = freqIt.lastFreq;
+      data.lastIdleValues[i] = idleIt.lastIdle;
     }
-
     return data;
   }
 
-  private async queryData(
-    start: time,
-    end: time,
-    bucketSize: duration,
-  ): Promise<QueryResult> {
-    const isCached = this.cachedBucketSize <= bucketSize;
-
-    if (isCached) {
-      return this.engine.query(`
-        select
-          cachedTsq / ${bucketSize} * ${bucketSize} as tsq,
-          min(minFreq) as minFreq,
-          max(maxFreq) as maxFreq,
-          value_at_max_ts(cachedTsq, lastFreq) as lastFreq,
-          value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue
-        from ${this.tableName('freq_idle_cached')}
-        where
-          cachedTsq >= ${start - this.maxDur} and
-          cachedTsq <= ${end}
-        group by tsq
-        order by tsq
-      `);
-    }
-    const minTsFreq = await this.engine.query(`
-      select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')}
-      where ts < ${start}
-    `);
-
-    let minTs = minTsFreq.iter({minTs: NUM}).minTs;
-    if (this.config.idleTrackId !== undefined) {
-      const minTsIdle = await this.engine.query(`
-        select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')}
-        where ts < ${start}
-      `);
-      minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs);
-    }
-
-    const geqConstraint =
-      this.config.idleTrackId === undefined
-        ? `ts >= ${minTs}`
-        : `source_geq(ts, ${minTs})`;
-    return this.engine.query(`
-      select
-        (ts + ${bucketSize / 2n}) / ${bucketSize} * ${bucketSize} as tsq,
-        min(freqValue) as minFreq,
-        max(freqValue) as maxFreq,
-        value_at_max_ts(ts, freqValue) as lastFreq,
-        value_at_max_ts(ts, idleValue) as lastIdleValue
-      from ${this.tableName('freq_idle')}
-      where
-        ${geqConstraint} and
-        ts <= ${end}
-      group by tsq
-      order by tsq
-    `);
-  }
-
-  private async queryMaxFrequency(): Promise<number> {
-    const result = await this.engine.query(`
-      select max(freqValue) as maxFreq
-      from ${this.tableName('freq')}
-    `);
-    return result.firstRow({maxFreq: NUM_NULL}).maxFreq ?? 0;
-  }
-
-  private async queryMaxSourceDur(): Promise<duration> {
-    const maxDurFreqResult = await this.engine.query(
-      `select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`,
-    );
-    const maxDur = maxDurFreqResult.firstRow({maxDur: LONG}).maxDur;
-    if (this.config.idleTrackId === undefined) {
-      return maxDur;
-    }
-
-    const maxDurIdleResult = await this.engine.query(
-      `select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`,
-    );
-    return BIMath.max(maxDur, maxDurIdleResult.firstRow({maxDur: LONG}).maxDur);
-  }
-
-  private async createFreqIdleViews() {
-    await this.engine.query(`create view ${this.tableName('freq')} as
-      select
-        ts,
-        dur,
-        value as freqValue
-      from experimental_counter_dur c
-      where track_id = ${this.config.freqTrackId};
-    `);
-
-    if (this.config.idleTrackId === undefined) {
-      await this.engine.query(`create view ${this.tableName('freq_idle')} as
-        select
-          ts,
-          dur,
-          -1 as idleValue,
-          freqValue
-        from ${this.tableName('freq')};
-      `);
-      return;
-    }
-
-    await this.engine.query(`
-      create view ${this.tableName('idle')} as
-      select
-        ts,
-        dur,
-        iif(value = 4294967295, -1, cast(value as int)) as idleValue
-      from experimental_counter_dur c
-      where track_id = ${this.config.idleTrackId};
-    `);
-
-    await this.engine.query(`
-      create virtual table ${this.tableName('freq_idle')}
-      using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
-    `);
-  }
-
-  private maximumValue() {
-    return Math.max(this.config.maximumValue ?? 0, this.maximumValueSeen);
-  }
-
   getHeight() {
     return MARGIN_TOP + RECT_HEIGHT;
   }
@@ -337,7 +216,7 @@
     const zeroY = MARGIN_TOP + RECT_HEIGHT;
 
     // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
-    let yMax = data.maximumValue;
+    let yMax = this.config.maximumValue;
     const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
     const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
     const pow10 = Math.pow(10, exp);
@@ -347,7 +226,6 @@
     // The values we have for cpufreq are in kHz so +1 to unitGroup.
     const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
 
-    // Draw the CPU frequency graph.
     const color = colorForCpu(this.config.cpu);
     let saturation = 45;
     if (globals.state.hoveredUtid !== -1) {
@@ -366,69 +244,69 @@
 
     const start = visibleWindowTime.start;
     const end = visibleWindowTime.end;
+
     const [rawStartIdx] = searchSegment(data.timestamps, start.toTime());
     const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
 
     const [, rawEndIdx] = searchSegment(data.timestamps, end.toTime());
     const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
 
-    ctx.beginPath();
-    const timestamp = Time.fromRaw(data.timestamps[startIdx]);
-    ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
+    // Draw the CPU frequency graph.
+    {
+      ctx.beginPath();
+      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
+      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
 
-    let lastDrawnY = zeroY;
-    for (let i = startIdx; i < endIdx; i++) {
-      const timestamp = Time.fromRaw(data.timestamps[i]);
-      const x = calculateX(timestamp);
+      let lastDrawnY = zeroY;
+      for (let i = startIdx; i < endIdx; i++) {
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = Math.max(0, calculateX(timestamp));
+        const minY = calculateY(data.minFreqKHz[i]);
+        const maxY = calculateY(data.maxFreqKHz[i]);
+        const lastY = calculateY(data.lastFreqKHz[i]);
 
-      const minY = calculateY(data.minFreqKHz[i]);
-      const maxY = calculateY(data.maxFreqKHz[i]);
-      const lastY = calculateY(data.lastFreqKHz[i]);
-
-      ctx.lineTo(x, lastDrawnY);
-      if (minY === maxY) {
-        assertTrue(lastY === minY);
-        ctx.lineTo(x, lastY);
-      } else {
-        ctx.lineTo(x, minY);
-        ctx.lineTo(x, maxY);
-        ctx.lineTo(x, lastY);
+        ctx.lineTo(x, lastDrawnY);
+        if (minY === maxY) {
+          assertTrue(lastY === minY);
+          ctx.lineTo(x, lastY);
+        } else {
+          ctx.lineTo(x, minY);
+          ctx.lineTo(x, maxY);
+          ctx.lineTo(x, lastY);
+        }
+        lastDrawnY = lastY;
       }
-      lastDrawnY = lastY;
+      ctx.lineTo(endPx, lastDrawnY);
+      ctx.lineTo(endPx, zeroY);
+      ctx.closePath();
+      ctx.fill();
+      ctx.stroke();
     }
-    // Find the end time for the last frequency event and then draw
-    // down to zero to show that we do not have data after that point.
-    const finalX = Math.min(calculateX(data.maxTsEnd), endPx);
-    ctx.lineTo(finalX, lastDrawnY);
-    ctx.lineTo(finalX, zeroY);
-    ctx.lineTo(endPx, zeroY);
-    ctx.closePath();
-    ctx.fill();
-    ctx.stroke();
 
     // Draw CPU idle rectangles that overlay the CPU freq graph.
     ctx.fillStyle = `rgba(240, 240, 240, 1)`;
+    {
+      for (let i = startIdx; i < endIdx; i++) {
+        if (data.lastIdleValues[i] < 0) {
+          continue;
+        }
 
-    for (let i = startIdx; i < endIdx; i++) {
-      if (data.lastIdleValues[i] < 0) {
-        continue;
+        // We intentionally don't use the floor function here when computing x
+        // coordinates. Instead we use floating point which prevents flickering as
+        // we pan and zoom; this relies on the browser anti-aliasing pixels
+        // correctly.
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = visibleTimeScale.timeToPx(timestamp);
+        const xEnd =
+          i === data.lastIdleValues.length - 1
+            ? endPx
+            : visibleTimeScale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
+
+        const width = xEnd - x;
+        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
+
+        ctx.fillRect(x, zeroY, width, height);
       }
-
-      // We intentionally don't use the floor function here when computing x
-      // coordinates. Instead we use floating point which prevents flickering as
-      // we pan and zoom; this relies on the browser anti-aliasing pixels
-      // correctly.
-      const timestamp = Time.fromRaw(data.timestamps[i]);
-      const x = visibleTimeScale.timeToPx(timestamp);
-      const xEnd =
-        i === data.lastIdleValues.length - 1
-          ? finalX
-          : visibleTimeScale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
-
-      const width = xEnd - x;
-      const height = calculateY(data.lastFreqKHz[i]) - zeroY;
-
-      ctx.fillRect(x, zeroY, width, height);
     }
 
     ctx.font = '10px Roboto Condensed';
@@ -504,6 +382,7 @@
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     const [left, right] = searchSegment(data.timestamps, time.toTime());
+
     this.hoveredTs =
       left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
     this.hoveredTsEnd =
@@ -529,16 +408,13 @@
     const maxCpuFreqResult = await engine.query(`
       select ifnull(max(value), 0) as freq
       from counter c
-      inner join cpu_counter_track t on c.track_id = t.id
+      join cpu_counter_track t on c.track_id = t.id
       where name = 'cpufreq';
     `);
     const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
 
     for (const cpu of cpus) {
-      // Only add a cpu freq track if we have
-      // cpu freq data.
-      // TODO(hjd): Find a way to display cpu idle
-      // events even if there are no cpu freq events.
+      // Only add a cpu freq track if we have cpu freq data.
       const cpuFreqIdleResult = await engine.query(`
         select
           id as cpuFreqId,
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 412ee6a..5c042dc 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -12,14 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {search, searchEq, searchSegment} from '../../base/binary_search';
 import {assertExists, assertTrue} from '../../base/logging';
 import {Duration, duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {calcCachedBucketSize} from '../../common/cache_utils';
 import {getLegacySelection} from '../../common/state';
 import {
   cropText,
@@ -43,6 +40,7 @@
   Track,
 } from '../../public';
 import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
 
@@ -68,13 +66,11 @@
   private utidHoveredInThisTrack = -1;
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
 
-  private uuid = uuidv4();
-  private cachedBucketSize = BIMath.INT64_MAX;
-  private maxDur: duration = 0n;
   private lastRowId = -1;
   private engine: EngineProxy;
   private cpu: number;
   private trackKey: string;
+  private trackUuid = uuidv4Sql();
 
   constructor(engine: EngineProxy, trackKey: string, cpu: number) {
     this.engine = engine;
@@ -82,62 +78,25 @@
     this.cpu = cpu;
   }
 
-  // Returns a valid SQL table name with the given prefix that should be unique
-  // for each track.
-  private tableName(prefix: string) {
-    // Derive table name from, since that is unique for each track.
-    // Track ID can be UUID but '-' is not valid for sql table name.
-    const idSuffix = this.uuid.split('-').join('_');
-    return `${prefix}_${idSuffix}`;
-  }
-
   async onCreate() {
     await this.engine.query(`
-      create view ${this.tableName('sched')} as
-      select
-        ts,
-        dur,
-        utid,
-        id,
-        dur = -1 as isIncomplete,
-        (case when priority < 100 then 1 else 0 end) as isRealtime
+      create virtual table cpu_slice_${this.trackUuid}
+      using __intrinsic_slice_mipmap((
+        select
+          id,
+          ts,
+          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
+          0 as depth
+        from sched
+        where cpu = ${this.cpu} and utid != 0
+      ));
+    `);
+    const it = await this.engine.query(`
+      select coalesce(max(id), -1) as lastRowId
       from sched
       where cpu = ${this.cpu} and utid != 0
     `);
-
-    const queryRes = await this.engine.query(`
-      select ifnull(max(dur), 0) as maxDur, count(1) as rowCount
-      from ${this.tableName('sched')}
-    `);
-
-    const queryLastSlice = await this.engine.query(`
-      select ifnull(max(id), -1) as lastSliceId from ${this.tableName('sched')}
-    `);
-    this.lastRowId = queryLastSlice.firstRow({lastSliceId: NUM}).lastSliceId;
-
-    const row = queryRes.firstRow({maxDur: LONG, rowCount: NUM});
-    this.maxDur = row.maxDur;
-    const rowCount = row.rowCount;
-    const bucketSize = calcCachedBucketSize(rowCount);
-    if (bucketSize === undefined) {
-      return;
-    }
-
-    await this.engine.query(`
-      create table ${this.tableName('sched_cached')} as
-      select
-        (ts + ${bucketSize / 2n}) / ${bucketSize} * ${bucketSize} as cached_tsq,
-        ts,
-        max(dur) as dur,
-        utid,
-        id,
-        isIncomplete,
-        isRealtime
-      from ${this.tableName('sched')}
-      group by cached_tsq, isIncomplete
-      order by cached_tsq
-    `);
-    this.cachedBucketSize = bucketSize;
+    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
   }
 
   async onUpdate() {
@@ -151,30 +110,16 @@
   ): Promise<Data> {
     assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
 
-    const isCached = this.cachedBucketSize <= resolution;
-    const queryTsq = isCached
-      ? `cached_tsq / ${resolution} * ${resolution}`
-      : `(ts + ${resolution / 2n}) / ${resolution} * ${resolution}`;
-    const queryTable = isCached
-      ? this.tableName('sched_cached')
-      : this.tableName('sched');
-    const constraintColumn = isCached ? 'cached_tsq' : 'ts';
-
     const queryRes = await this.engine.query(`
       select
-        ${queryTsq} as tsq,
-        ts,
-        max(dur) as dur,
-        utid,
-        id,
-        isIncomplete,
-        isRealtime
-      from ${queryTable}
-      where
-        ${constraintColumn} >= ${start - this.maxDur} and
-        ${constraintColumn} <= ${end}
-      group by tsq, isIncomplete
-      order by tsq
+        (z.ts / ${resolution}) * ${resolution} as ts,
+        max(z.dur, ${resolution}) as dur,
+        s.utid,
+        s.id,
+        s.dur = -1 as isIncomplete,
+        ifnull(s.priority < 100, 0) as isRealtime
+      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
+      cross join sched s using (id)
     `);
 
     const numRows = queryRes.numRows();
@@ -192,7 +137,6 @@
     };
 
     const it = queryRes.iter({
-      tsq: LONG,
       ts: LONG,
       dur: LONG,
       utid: NUM,
@@ -201,19 +145,11 @@
       isRealtime: NUM,
     });
     for (let row = 0; it.valid(); it.next(), row++) {
-      const startQ = it.tsq;
       const start = it.ts;
       const dur = it.dur;
-      const end = start + dur;
 
-      // If the slice is incomplete, the end calculated later.
-      if (!it.isIncomplete) {
-        const minEnd = startQ + resolution;
-        const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
-        slices.ends[row] = endQ;
-      }
-
-      slices.starts[row] = startQ;
+      slices.starts[row] = start;
+      slices.ends[row] = start + dur;
       slices.utids[row] = it.utid;
       slices.ids[row] = it.id;
 
@@ -225,26 +161,13 @@
         slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
       }
     }
-
-    // If the slice is incomplete and it is the last slice in the track, the end
-    // of the slice would be the end of the visible window. Otherwise we end the
-    // slice with the beginning the next one.
-    for (let row = 0; row < slices.length; row++) {
-      if (!(slices.flags[row] & CPU_SLICE_FLAGS_INCOMPLETE)) {
-        continue;
-      }
-      const endTime = row === slices.length - 1 ? end : slices.starts[row + 1];
-      const minEnd = slices.starts[row] + resolution;
-      const endQ = BIMath.max(BIMath.quant(endTime, resolution), minEnd);
-      slices.ends[row] = endQ;
-    }
     return slices;
   }
 
   async onDestroy() {
     if (this.engine.isAlive) {
       await this.engine.query(
-        `drop table if exists ${this.tableName('sched_cached')}`,
+        `drop table if exists cpu_slice_${this.trackUuid}`,
       );
     }
     this.fetcher.dispose();
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index 4ea16dc..a53e4dc 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -90,13 +90,12 @@
     // TODO(altimin): Support removing this table when the track is closed.
     const dur = sliceColumns.dur === '0' ? 0 : sliceColumns.dur;
     await this.engine.query(`
-      create table ${this.sqlTableName} as
+      create perfetto table ${this.sqlTableName} as
       with data${dataColumns} as (
         ${data.sqlSource}
       ),
       prepared_data as (
         select
-          row_number() over () as id,
           ${sliceColumns.ts} as ts,
           ifnull(cast(${dur} as int), -1) as dur,
           printf('%s', ${sliceColumns.name}) as name
@@ -105,6 +104,7 @@
         from data
       )
       select
+        row_number() over (order by ts) as id,
         *
       from prepared_data
       order by ts;`);
diff --git a/ui/src/tracks/frames/index.ts b/ui/src/tracks/frames/index.ts
index 47e88ff..2cc877f 100644
--- a/ui/src/tracks/frames/index.ts
+++ b/ui/src/tracks/frames/index.ts
@@ -31,26 +31,17 @@
   async addExpectedFrames(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where process_track.name = "Expected Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
-  `);
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name = "Expected Timeline"
+    `);
 
     const it = result.iter({
       upid: NUM,
@@ -58,7 +49,7 @@
       trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
-      maxDepth: NUM_NULL,
+      maxDepth: NUM,
     });
 
     for (; it.valid(); it.next()) {
@@ -70,11 +61,6 @@
       const pid = it.pid;
       const maxDepth = it.maxDepth;
 
-      if (maxDepth === null) {
-        // If there are no slices in this track, skip it.
-        continue;
-      }
-
       const displayName = getTrackName({
         name: trackName,
         upid,
@@ -103,26 +89,17 @@
   async addActualFrames(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
-      with process_async_tracks as materialized (
-        select
-          process_track.upid as upid,
-          process_track.name as trackName,
-          process.name as processName,
-          process.pid as pid,
-          group_concat(process_track.id) as trackIds,
-          count(1) as trackCount
-        from process_track
-        join process using(upid)
-        where process_track.name = "Actual Timeline"
-        group by
-          process_track.upid,
-          process_track.name
-      )
       select
-        t.*,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from process_async_tracks t;
-  `);
+        upid,
+        t.name as trackName,
+        t.track_ids as trackIds,
+        process.name as processName,
+        process.pid as pid,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
+      from _process_track_summary_by_upid_and_name t
+      join process using(upid)
+      where t.name = "Actual Timeline"
+    `);
 
     const it = result.iter({
       upid: NUM,
diff --git a/ui/src/tracks/ftrace/ftrace_explorer.ts b/ui/src/tracks/ftrace/ftrace_explorer.ts
index 6884d70..5e7eb32 100644
--- a/ui/src/tracks/ftrace/ftrace_explorer.ts
+++ b/ui/src/tracks/ftrace/ftrace_explorer.ts
@@ -24,27 +24,18 @@
   PopupMultiSelect,
 } from '../../widgets/multiselect';
 import {PopupPosition} from '../../widgets/popup';
-import {VirtualScrollContainer} from '../../widgets/virtual_scroll_container';
 
 import {globals} from '../../frontend/globals';
 import {Timestamp} from '../../frontend/widgets/timestamp';
 import {FtraceFilter, FtraceStat} from './common';
-import {
-  createStore,
-  EngineProxy,
-  LONG,
-  NUM,
-  Store,
-  STR,
-  STR_NULL,
-} from '../../public';
+import {EngineProxy, LONG, NUM, Store, STR, STR_NULL} from '../../public';
 import {raf} from '../../core/raf_scheduler';
 import {AsyncLimiter} from '../../base/async_limiter';
 import {Monitor} from '../../base/monitor';
 import {Button} from '../../widgets/button';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
 
 const ROW_H = 20;
-const PAGE_SIZE = 250;
 
 interface FtraceExplorerAttrs {
   cache: FtraceExplorerCache;
@@ -69,8 +60,8 @@
 }
 
 interface Pagination {
-  page: number;
-  pageCount: number;
+  offset: number;
+  count: number;
 }
 
 export interface FtraceExplorerCache {
@@ -104,10 +95,10 @@
 }
 
 export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
-  private readonly paginationStore = createStore<Pagination>({
-    page: 0,
-    pageCount: 0,
-  });
+  private pagination: Pagination = {
+    offset: 0,
+    count: 0,
+  };
   private readonly monitor: Monitor;
   private readonly queryLimiter = new AsyncLimiter();
 
@@ -119,7 +110,6 @@
       () => globals.state.frontendLocalState.visibleState.start,
       () => globals.state.frontendLocalState.visibleState.end,
       () => attrs.filterStore.state,
-      () => this.paginationStore.state,
     ]);
 
     if (attrs.cache.state === 'blank') {
@@ -136,60 +126,85 @@
   }
 
   view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
-    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();
-      }),
-    );
+    this.monitor.ifStateChanged(() => {
+      this.reloadData(attrs);
+    });
 
     return m(
       DetailsShell,
       {
         title: this.renderTitle(),
         buttons: this.renderFilterPanel(attrs),
+        fillParent: true,
       },
-      m(
-        VirtualScrollContainer,
-        {
-          onScroll: this.onScroll.bind(this),
+      m(VirtualTable, {
+        className: 'pf-ftrace-explorer',
+        columns: [
+          {header: 'ID', width: '5em'},
+          {header: 'Timestamp', width: '13em'},
+          {header: 'Name', width: '24em'},
+          {header: 'CPU', width: '3em'},
+          {header: 'Process', width: '24em'},
+          {header: 'Args', width: '200em'},
+        ],
+        firstRowOffset: this.data?.offset ?? 0,
+        numRows: this.data?.numEvents ?? 0,
+        rowHeight: ROW_H,
+        rows: this.renderData(),
+        onReload: (offset, count) => {
+          this.pagination = {offset, count};
+          this.reloadData(attrs);
         },
-        m('.ftrace-panel', this.renderRows()),
-      ),
+        onRowHover: this.onRowOver.bind(this),
+        onRowOut: this.onRowOut.bind(this),
+      }),
     );
   }
 
-  onScroll(scrollContainer: HTMLElement) {
-    const paginationState = this.paginationStore.state;
-    const prevPage = paginationState.page;
-    const prevPageCount = paginationState.pageCount;
-
-    const visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
-    const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
-
-    // Work out which "page" we're on
-    const page = Math.max(0, Math.floor(visibleRowOffset / PAGE_SIZE) - 1);
-    const pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2;
-
-    if (page !== prevPage || pageCount !== prevPageCount) {
-      this.paginationStore.edit((draft) => {
-        draft.page = page;
-        draft.pageCount = pageCount;
-      });
+  private reloadData(attrs: FtraceExplorerAttrs): void {
+    this.queryLimiter.schedule(async () => {
+      this.data = await lookupFtraceEvents(
+        attrs.engine,
+        this.pagination.offset,
+        this.pagination.count,
+        attrs.filterStore.state,
+      );
       raf.scheduleFullRedraw();
+    });
+  }
+
+  private renderData(): VirtualTableRow[] {
+    if (!this.data) {
+      return [];
+    }
+
+    return this.data.events.map((event) => {
+      const {ts, name, cpu, process, args, id} = event;
+      const timestamp = m(Timestamp, {ts});
+      const color = colorForFtrace(name).base.cssString;
+
+      return {
+        id,
+        cells: [
+          id,
+          timestamp,
+          m('', m('span.colour', {style: {background: color}}), name),
+          cpu,
+          process,
+          args,
+        ],
+      };
+    });
+  }
+
+  private onRowOver(id: number) {
+    const event = this.data?.events.find((event) => event.id === id);
+    if (event) {
+      globals.dispatch(Actions.setHoverCursorTimestamp({ts: event.ts}));
     }
   }
 
-  onRowOver(ts: time) {
-    globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
-  }
-
-  onRowOut() {
+  private onRowOut() {
     globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
   }
 
@@ -206,7 +221,6 @@
     if (attrs.cache.state !== 'valid') {
       return m(Button, {
         label: 'Filter',
-        minimal: true,
         disabled: true,
         loading: true,
       });
@@ -225,7 +239,6 @@
 
     return m(PopupMultiSelect, {
       label: 'Filter',
-      minimal: true,
       icon: 'filter_list_alt',
       popupPosition: PopupPosition.Top,
       options,
@@ -244,55 +257,6 @@
       },
     });
   }
-
-  // Render all the rows including the first title row
-  private renderRows() {
-    const data = this.data;
-    const rows: m.Children = [];
-
-    rows.push(
-      m(
-        `.row`,
-        m('.cell.row-header', 'Timestamp'),
-        m('.cell.row-header', 'Name'),
-        m('.cell.row-header', 'CPU'),
-        m('.cell.row-header', 'Process'),
-        m('.cell.row-header', 'Args'),
-      ),
-    );
-
-    if (data) {
-      const {events, offset, numEvents} = data;
-      for (let i = 0; i < events.length; i++) {
-        const {ts, name, cpu, process, args} = events[i];
-
-        const timestamp = m(Timestamp, {ts});
-
-        const rank = i + offset;
-
-        const color = colorForFtrace(name).base.cssString;
-
-        rows.push(
-          m(
-            `.row`,
-            {
-              style: {top: `${(rank + 1.0) * ROW_H}px`},
-              onmouseover: this.onRowOver.bind(this, ts),
-              onmouseout: this.onRowOut.bind(this),
-            },
-            m('.cell', timestamp),
-            m('.cell', m('span.colour', {style: {background: color}}), name),
-            m('.cell', cpu),
-            m('.cell', process),
-            m('.cell', args),
-          ),
-        );
-      }
-      return m('.rows', {style: {height: `${numEvents * ROW_H}px`}}, rows);
-    } else {
-      return m('.rows', rows);
-    }
-  }
 }
 
 async function lookupFtraceEvents(
diff --git a/ui/src/tracks/ftrace/ftrace_track.ts b/ui/src/tracks/ftrace/ftrace_track.ts
index 26c3f38..7b9def9 100644
--- a/ui/src/tracks/ftrace/ftrace_track.ts
+++ b/ui/src/tracks/ftrace/ftrace_track.ts
@@ -74,7 +74,6 @@
     const queryRes = await this.engine.query(`
       select
         cast(ts / ${resolution} as integer) * ${resolution} as tsQuant,
-        type,
         name
       from ftrace_event
       where
@@ -93,7 +92,7 @@
       names: [],
     };
 
-    const it = queryRes.iter({tsQuant: LONG, type: STR, name: STR});
+    const it = queryRes.iter({tsQuant: LONG, name: STR});
     for (let row = 0; it.valid(); it.next(), row++) {
       result.timestamps[row] = it.tsQuant;
       result.names[row] = it.name;
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index 7542232..3cc0616 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -97,7 +97,7 @@
 
     ctx.registerCommand({
       id: 'perfetto.FtraceRaw#ShowFtraceTab',
-      name: 'Show Ftrace Tab',
+      name: 'Show ftrace tab',
       callback: () => {
         ctx.tabs.showTab(ftraceTabUri);
       },
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index 47a93b0..ea6845f 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -12,17 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  STR,
-  STR_NULL,
-} from '../../trace_processor/query_result';
-import {assertExists} from '../../base/logging';
+import {NUM, NUM_NULL} from '../../trace_processor/query_result';
 
 import {
   Config as ProcessSchedulingTrackConfig,
@@ -37,226 +28,101 @@
 
 // This plugin now manages both process "scheduling" and "summary" tracks.
 class ProcessSummaryPlugin implements Plugin {
-  private upidToUuid = new Map<number, string>();
-  private utidToUuid = new Map<number, string>();
-
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addProcessTrackGroups(ctx);
     await this.addKernelThreadSummary(ctx);
   }
 
   private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
-    this.upidToUuid.clear();
-    this.utidToUuid.clear();
-
-    // We want to create groups of tracks in a specific order.
-    // The tracks should be grouped:
-    //    by upid
-    //    or (if upid is null) by utid
-    // the groups should be sorted by:
-    //  Chrome-based process rank based on process names (e.g. Browser)
-    //  has a heap profile or not
-    //  total cpu time *for the whole parent process*
-    //  process name
-    //  upid
-    //  thread name
-    //  utid
     const result = await ctx.engine.query(`
-    with candidateThreadsAndProcesses as materialized (
-      select upid, 0 as utid from process_track
-      union
-      select upid, 0 as utid from process_counter_track
-      union
-      select upid, utid from thread_counter_track join thread using(utid)
-      union
-      select upid, utid from thread_track join thread using(utid)
-      union
-      select upid, utid from (
-        select distinct utid from sched
-      ) join thread using(utid) group by utid
-      union
-      select upid, 0 as utid from (
-        select distinct utid from perf_sample where callsite_id is not null
-      ) join thread using (utid)
-      union
-      select upid, utid from (
-        select distinct utid from cpu_profile_stack_sample
-      ) join thread using(utid)
-      union
-      select upid as upid, 0 as utid from heap_profile_allocation
-      union
-      select upid as upid, 0 as utid from heap_graph_object
-    ),
-    schedSummary as materialized (
-      select
-        upid,
-        sum(thread_total_dur) as total_dur,
-        max(thread_max_dur) as total_max_dur,
-        sum(thread_event_count) as total_event_count
+      select *
       from (
         select
-          utid,
-          sum(dur) as thread_total_dur,
-          max(dur) as thread_max_dur,
-          count() as thread_event_count
-        from sched where dur != -1 and utid != 0
-        group by utid
+          _process_available_info_summary.upid,
+          null as utid,
+          pid,
+          null as tid,
+          process.name as processName,
+          null as threadName,
+          sum_running_dur > 0 as hasSched,
+          android_process_metadata.debuggable as isDebuggable
+        from _process_available_info_summary
+        join process using(upid)
+        left join android_process_metadata using(upid)
       )
-      join thread using (utid)
-      group by upid
-    ),
-    sliceSum as materialized (
-      select
-        process.upid as upid,
-        sum(cnt) as sliceCount
-      from (select track_id, count(*) as cnt from slice group by track_id)
-        left join thread_track on track_id = thread_track.id
-        left join thread on thread_track.utid = thread.utid
-        left join process_track on track_id = process_track.id
-        join process on process.upid = thread.upid
-          or process_track.upid = process.upid
-      where process.upid is not null
-      group by process.upid
-    )
-    select
-      the_tracks.upid,
-      the_tracks.utid,
-      total_dur as hasSched,
-      total_max_dur as schedMaxDur,
-      total_event_count as schedEventCount,
-      hasHeapProfiles,
-      process.pid as pid,
-      thread.tid as tid,
-      process.name as processName,
-      thread.name as threadName,
-      package_list.debuggable as isDebuggable,
-      ifnull((
-        select group_concat(string_value)
-        from args
-        where
-          process.arg_set_id is not null and
-          arg_set_id = process.arg_set_id and
-          flat_key = 'chrome.process_label'
-      ), '') AS chromeProcessLabels,
-      (case process.name
-         when 'Browser' then 3
-         when 'Gpu' then 2
-         when 'Renderer' then 1
-         else 0
-      end) as chromeProcessRank
-    from candidateThreadsAndProcesses the_tracks
-    left join schedSummary using(upid)
-    left join (
-      select
-        distinct(upid) as upid,
-        true as hasHeapProfiles
-      from heap_profile_allocation
-      union
-      select
-        distinct(upid) as upid,
-        true as hasHeapProfiles
-      from heap_graph_object
-    ) using (upid)
-    left join (
-      select
-        thread.upid as upid,
-        sum(cnt) as perfSampleCount
+      union all
+      select *
       from (
-          select utid, count(*) as cnt
-          from perf_sample where callsite_id is not null
-          group by utid
-      ) join thread using (utid)
-      group by thread.upid
-    ) using (upid)
-    left join sliceSum using (upid)
-    left join thread using(utid)
-    left join process using(upid)
-    left join package_list using(uid)
-    order by
-      chromeProcessRank desc,
-      hasHeapProfiles desc,
-      perfSampleCount desc,
-      total_dur desc,
-      sliceCount desc,
-      processName asc nulls last,
-      the_tracks.upid asc nulls last,
-      threadName asc nulls last,
-      the_tracks.utid asc nulls last;
+        select
+          null,
+          utid,
+          null as pid,
+          tid,
+          null as processName,
+          thread.name threadName,
+          sum_running_dur > 0 as hasSched,
+          0 as isDebuggable
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is null
+      )
   `);
 
     const it = result.iter({
-      utid: NUM,
       upid: NUM_NULL,
-      tid: NUM_NULL,
+      utid: NUM_NULL,
       pid: NUM_NULL,
-      threadName: STR_NULL,
-      processName: STR_NULL,
+      tid: NUM_NULL,
       hasSched: NUM_NULL,
-      schedMaxDur: LONG_NULL,
-      schedEventCount: NUM_NULL,
-      hasHeapProfiles: NUM_NULL,
       isDebuggable: NUM_NULL,
-      chromeProcessLabels: STR,
     });
     for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const tid = it.tid;
       const upid = it.upid;
+      const utid = it.utid;
       const pid = it.pid;
+      const tid = it.tid;
       const hasSched = Boolean(it.hasSched);
-      const schedMaxDur = it.schedMaxDur;
-      const schedEventCount = it.schedEventCount;
       const isDebuggable = Boolean(it.isDebuggable);
 
       // Group by upid if present else by utid.
-      let pUuid =
-        upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
-      // These should only happen once for each track group.
-      if (pUuid === undefined) {
-        pUuid = this.getOrCreateUuid(utid, upid);
-        const pidForColor = pid ?? tid ?? upid ?? utid ?? 0;
-        const type = hasSched ? 'schedule' : 'summary';
-        const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
+      const pidForColor = pid ?? tid ?? upid ?? utid ?? 0;
+      const type = hasSched ? 'schedule' : 'summary';
+      const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
 
-        if (hasSched) {
-          const config: ProcessSchedulingTrackConfig = {
-            pidForColor,
-            upid,
-            utid,
-          };
+      if (hasSched) {
+        const config: ProcessSchedulingTrackConfig = {
+          pidForColor,
+          upid,
+          utid,
+        };
 
-          ctx.registerTrack({
-            uri,
-            displayName: `${upid === null ? tid : pid} schedule`,
-            kind: PROCESS_SCHEDULING_TRACK_KIND,
-            tags: {
-              isDebuggable,
-            },
-            trackFactory: () =>
-              new ProcessSchedulingTrack(
-                ctx.engine,
-                config,
-                assertExists(schedMaxDur),
-                assertExists(schedEventCount),
-              ),
-          });
-        } else {
-          const config: ProcessSummaryTrackConfig = {
-            pidForColor,
-            upid,
-            utid,
-          };
+        ctx.registerTrack({
+          uri,
+          displayName: `${upid === null ? tid : pid} schedule`,
+          kind: PROCESS_SCHEDULING_TRACK_KIND,
+          tags: {
+            isDebuggable,
+          },
+          trackFactory: () => {
+            return new ProcessSchedulingTrack(ctx.engine, config);
+          },
+        });
+      } else {
+        const config: ProcessSummaryTrackConfig = {
+          pidForColor,
+          upid,
+          utid,
+        };
 
-          ctx.registerTrack({
-            uri,
-            displayName: `${upid === null ? tid : pid} summary`,
-            kind: PROCESS_SUMMARY_TRACK,
-            tags: {
-              isDebuggable,
-            },
-            trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
-          });
-        }
+        ctx.registerTrack({
+          uri,
+          displayName: `${upid === null ? tid : pid} summary`,
+          kind: PROCESS_SUMMARY_TRACK,
+          tags: {
+            isDebuggable,
+          },
+          trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
+        });
       }
     }
   }
@@ -313,25 +179,6 @@
       trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
     });
   }
-
-  private getOrCreateUuid(utid: number, upid: number | null) {
-    let uuid = this.getUuidUnchecked(utid, upid);
-    if (uuid === undefined) {
-      uuid = uuidv4();
-      if (upid === null) {
-        this.utidToUuid.set(utid, uuid);
-      } else {
-        this.upidToUuid.set(upid, uuid);
-      }
-    }
-    return uuid;
-  }
-
-  getUuidUnchecked(utid: number, upid: number | null) {
-    return upid === null
-      ? this.utidToUuid.get(utid)
-      : this.upidToUuid.get(upid);
-  }
 }
 
 export const plugin: PluginDescriptor = {
diff --git a/ui/src/tracks/process_summary/process_scheduling_track.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts
index 07400ca..9725c29 100644
--- a/ui/src/tracks/process_summary/process_scheduling_track.ts
+++ b/ui/src/tracks/process_summary/process_scheduling_track.ts
@@ -12,14 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {searchEq, searchRange} from '../../base/binary_search';
-import {assertTrue} from '../../base/logging';
+import {assertExists, assertTrue} from '../../base/logging';
 import {duration, time, Time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {calcCachedBucketSize} from '../../common/cache_utils';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 import {Color} from '../../core/color';
 import {colorForThread} from '../../core/colorizer';
@@ -30,6 +27,7 @@
 import {PanelSize} from '../../frontend/panel';
 import {EngineProxy, Track} from '../../public';
 import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
 
@@ -50,8 +48,8 @@
 
 export interface Config {
   pidForColor: number;
-  upid: null | number;
-  utid: number;
+  upid: number | null;
+  utid: number | null;
 }
 
 export class ProcessSchedulingTrack implements Track {
@@ -59,60 +57,60 @@
   private utidHoveredInThisTrack = -1;
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
   private maxCpu = 0;
-  private maxDur;
-  private eventCount;
-  private cachedBucketSize = BIMath.INT64_MAX;
   private engine: EngineProxy;
-  private uuid = uuidv4();
+  private trackUuid = uuidv4Sql();
   private config: Config;
 
-  constructor(
-    engine: EngineProxy,
-    config: Config,
-    maxDur: duration,
-    eventCount: number,
-  ) {
+  constructor(engine: EngineProxy, config: Config) {
     this.engine = engine;
     this.config = config;
-    this.maxDur = maxDur;
-    this.eventCount = eventCount;
-  }
-
-  // Returns a valid SQL table name with the given prefix that should be unique
-  // for each track.
-  private tableName(prefix: string) {
-    // Derive table name from, since that is unique for each track.
-    // Track ID can be UUID but '-' is not valid for sql table name.
-    const idSuffix = this.uuid.split('-').join('_');
-    return `${prefix}_${idSuffix}`;
   }
 
   async onCreate(): Promise<void> {
-    await this.createSchedView();
-
     const cpus = await this.engine.getCpus();
 
     // A process scheduling track should only exist in a trace that has cpus.
     assertTrue(cpus.length > 0);
     this.maxCpu = Math.max(...cpus) + 1;
 
-    const bucketSize = calcCachedBucketSize(this.eventCount);
-    if (bucketSize === undefined) {
-      return;
+    if (this.config.upid !== null) {
+      await this.engine.query(`
+        create virtual table process_scheduling_${this.trackUuid}
+        using __intrinsic_slice_mipmap((
+          select
+            id,
+            ts,
+            iif(
+              dur = -1,
+              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
+              dur
+            ) as dur,
+            cpu as depth
+          from experimental_sched_upid
+          where
+            utid != 0 and
+            upid = ${this.config.upid}
+        ));
+      `);
+    } else {
+      assertExists(this.config.utid);
+      await this.engine.query(`
+        create virtual table process_scheduling_${this.trackUuid}
+        using __intrinsic_slice_mipmap((
+          select
+            id,
+            ts,
+            iif(
+              dur = -1,
+              lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts,
+              dur
+            ) as dur,
+            cpu as depth
+          from sched
+          where utid = ${this.config.utid}
+        ));
+      `);
     }
-    await this.engine.query(`
-      create table ${this.tableName('process_sched_cached')} as
-      select
-        (ts + ${bucketSize / 2n}) / ${bucketSize} * ${bucketSize} as cached_tsq,
-        ts,
-        max(dur) as dur,
-        cpu,
-        utid
-      from ${this.tableName('process_sched')}
-      group by cached_tsq, cpu
-      order by cached_tsq, cpu
-    `);
-    this.cachedBucketSize = bucketSize;
   }
 
   async onUpdate(): Promise<void> {
@@ -121,6 +119,11 @@
 
   async onDestroy(): Promise<void> {
     this.fetcher.dispose();
+    if (this.engine.isAlive) {
+      await this.engine.query(`
+        drop table process_scheduling_${this.trackUuid}
+      `);
+    }
   }
 
   async onBoundsChange(
@@ -128,8 +131,6 @@
     end: time,
     resolution: duration,
   ): Promise<Data> {
-    assertTrue(this.config.upid !== null);
-
     // Resolution must always be a power of 2 for this logic to work
     assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
 
@@ -149,7 +150,6 @@
     };
 
     const it = queryRes.iter({
-      tsq: LONG,
       ts: LONG,
       dur: LONG,
       cpu: NUM,
@@ -157,61 +157,35 @@
     });
 
     for (let row = 0; it.valid(); it.next(), row++) {
-      const startQ = Time.fromRaw(it.tsq);
       const start = Time.fromRaw(it.ts);
       const dur = it.dur;
       const end = Time.add(start, dur);
-      const minEnd = Time.add(startQ, resolution);
-      const endQ = Time.max(Time.quant(end, resolution), minEnd);
 
-      slices.starts[row] = startQ;
-      slices.ends[row] = endQ;
+      slices.starts[row] = start;
+      slices.ends[row] = end;
       slices.cpus[row] = it.cpu;
       slices.utids[row] = it.utid;
-      slices.end = Time.max(endQ, slices.end);
+      slices.end = Time.max(end, slices.end);
     }
     return slices;
   }
 
-  private queryData(
+  private async queryData(
     start: time,
     end: time,
     bucketSize: duration,
   ): Promise<QueryResult> {
-    const isCached = this.cachedBucketSize <= bucketSize;
-    const tsq = isCached
-      ? `cached_tsq / ${bucketSize} * ${bucketSize}`
-      : `(ts + ${bucketSize / 2n}) / ${bucketSize} * ${bucketSize}`;
-    const queryTable = isCached
-      ? this.tableName('process_sched_cached')
-      : this.tableName('process_sched');
-    const constraintColumn = isCached ? 'cached_tsq' : 'ts';
-
-    // The mouse move handler depends on slices being sorted by cpu then tsq
     return this.engine.query(`
       select
-        ${tsq} as tsq,
-        ts,
-        max(dur) as dur,
-        cpu,
+        (z.ts / ${bucketSize}) * ${bucketSize} as ts,
+        iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur,
+        s.id,
+        z.depth as cpu,
         utid
-      from ${queryTable}
-      where
-        ${constraintColumn} >= ${start - this.maxDur} and
-        ${constraintColumn} <= ${end}
-      group by tsq, cpu
-      order by cpu, tsq
-    `);
-  }
-
-  private async createSchedView() {
-    await this.engine.query(`
-      create view ${this.tableName('process_sched')} as
-      select ts, dur, cpu, utid
-      from experimental_sched_upid
-      where
-        utid != 0 and
-        upid = ${this.config.upid}
+      from process_scheduling_${this.trackUuid}(
+        ${start}, ${end}, ${bucketSize}
+      ) z
+      cross join sched s using (id)
     `);
   }
 
@@ -252,10 +226,9 @@
       const utid = data.utids[i];
       const cpu = data.cpus[i];
 
-      const rectStart = visibleTimeScale.timeToPx(tStart);
-      const rectEnd = visibleTimeScale.timeToPx(tEnd);
-      const rectWidth = rectEnd - rectStart;
-      if (rectWidth < 0.3) continue;
+      const rectStart = Math.floor(visibleTimeScale.timeToPx(tStart));
+      const rectEnd = Math.floor(visibleTimeScale.timeToPx(tEnd));
+      const rectWidth = Math.max(1, rectEnd - rectStart);
 
       const threadInfo = globals.threads.get(utid);
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@@ -277,7 +250,7 @@
       }
       ctx.fillStyle = color.cssString;
       const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu;
-      ctx.fillRect(rectStart, y, rectEnd - rectStart, cpuTrackHeight);
+      ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight);
     }
 
     const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts
index 6a8e687..7acf36b 100644
--- a/ui/src/tracks/process_summary/process_summary_track.ts
+++ b/ui/src/tracks/process_summary/process_summary_track.ts
@@ -15,7 +15,7 @@
 import {v4 as uuidv4} from 'uuid';
 
 import {BigintMath} from '../../base/bigint_math';
-import {assertFalse} from '../../base/logging';
+import {assertExists, assertFalse} from '../../base/logging';
 import {duration, Time, time} from '../../base/time';
 import {colorForTid} from '../../core/colorizer';
 import {LIMIT, TrackData} from '../../common/track_data';
@@ -37,7 +37,7 @@
 export interface Config {
   pidForColor: number;
   upid: number | null;
-  utid: number;
+  utid: number | null;
 }
 
 const MARGIN_TOP = 5;
@@ -70,7 +70,7 @@
       `create virtual table ${this.tableName('window')} using window;`,
     );
 
-    let utids = [this.config.utid];
+    let utids: number[];
     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
     if (this.config.upid) {
       const threadQuery = await this.engine.query(
@@ -80,6 +80,8 @@
       for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
         utids.push(it.utid);
       }
+    } else {
+      utids = [assertExists(this.config.utid)];
     }
 
     const trackQuery = await this.engine.query(
diff --git a/ui/src/tracks/sched/index.ts b/ui/src/tracks/sched/index.ts
index a8b4b76..ccebaa1 100644
--- a/ui/src/tracks/sched/index.ts
+++ b/ui/src/tracks/sched/index.ts
@@ -44,18 +44,18 @@
   onActivate(ctx: PluginContext): void {
     ctx.registerCommand({
       id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
-      name: 'Add runnable thread count track',
+      name: 'Add track: runnable thread count',
       callback: () => addRunnableThreadCountTrack(),
     });
     ctx.registerCommand({
       id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
-      name: 'Add active CPU count track',
+      name: 'Add track: active CPU count',
       callback: () => addActiveCPUCountTrack(),
     });
     for (const cpuType of ['big', 'little', 'mid']) {
       ctx.registerCommand({
         id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
-        name: `Add active ${cpuType} CPU count track`,
+        name: `Add track: active ${cpuType} CPU count`,
         callback: () => addActiveCPUCountTrack(cpuType),
       });
     }
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index b38eb1d..73e2faf 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -32,14 +32,14 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
-      with ts_distinct as materialized (select distinct utid from thread_state)
       select
         utid,
         upid,
         tid,
         thread.name as threadName
       from thread
-      where utid != 0 and utid in ts_distinct`);
+      join _sched_summary using (utid)
+    `);
 
     const it = result.iter({
       utid: NUM,
diff --git a/ui/src/tracks/thread_state/thread_state_v2.ts b/ui/src/tracks/thread_state/thread_state_v2.ts
index 4ab000f..1d0b31f 100644
--- a/ui/src/tracks/thread_state/thread_state_v2.ts
+++ b/ui/src/tracks/thread_state/thread_state_v2.ts
@@ -55,12 +55,7 @@
   }
 
   getSqlSource(): string {
-    // Do not display states:
-    //   'x' (dead), 'S' (sleeping), 'I' (idle kernel thread).
-    // Note: Thread state tracks V1 basically ignores incomplete slices, faking
-    // their duration as 1 instead. Let's just do this here as well for now to
-    // achieve feature parity with tracks V1 and tackle the issue of overlapping
-    // incomplete slices later.
+    // Do not display states: 'S' (sleeping), 'I' (idle kernel thread).
     return `
       select
         id,
@@ -73,7 +68,7 @@
       from thread_state
       where
         utid = ${this.utid} and
-        state not in ('x', 'S', 'I')
+        state not in ('S', 'I')
     `;
   }
 
diff --git a/ui/src/tracks/visualised_args/index.ts b/ui/src/tracks/visualised_args/index.ts
index d2ede0c..9e34de3 100644
--- a/ui/src/tracks/visualised_args/index.ts
+++ b/ui/src/tracks/visualised_args/index.ts
@@ -100,7 +100,6 @@
       },
       icon: Icons.Close,
       title: 'Close',
-      minimal: true,
       compact: true,
     });
   }
diff --git a/ui/src/widgets/button.ts b/ui/src/widgets/button.ts
index 276b2f4..6f6a8c0 100644
--- a/ui/src/widgets/button.ts
+++ b/ui/src/widgets/button.ts
@@ -16,7 +16,7 @@
 
 import {classNames} from '../base/classnames';
 
-import {HTMLAttrs, HTMLButtonAttrs} from './common';
+import {HTMLAttrs, HTMLButtonAttrs, Intent, classForIntent} from './common';
 import {Icon} from './icon';
 import {Popup} from './popup';
 import {Spinner} from './spinner';
@@ -31,9 +31,6 @@
   // Use minimal padding, reducing the overall size of the button by a few px.
   // Defaults to false.
   compact?: boolean;
-  // Reduces button decorations.
-  // Defaults to false.
-  minimal?: boolean;
   // Optional right icon.
   rightIcon?: string;
   // List of space separated class names forwarded to the icon.
@@ -47,6 +44,9 @@
   // Whether to use a filled icon
   // Defaults to false;
   iconFilled?: boolean;
+  // Indicate button colouring by intent.
+  // Defaults to undefined aka "None"
+  intent?: Intent;
 }
 
 interface IconButtonAttrs extends CommonAttrs {
@@ -69,11 +69,11 @@
       icon,
       active,
       compact,
-      minimal,
       rightIcon,
       className,
       dismissPopup,
       iconFilled,
+      intent = Intent.None,
       ...htmlAttrs
     } = attrs;
 
@@ -82,7 +82,7 @@
     const classes = classNames(
       active && 'pf-active',
       compact && 'pf-compact',
-      minimal && 'pf-minimal',
+      classForIntent(intent),
       icon && !label && 'pf-icon-only',
       dismissPopup && Popup.DISMISS_POPUP_GROUP_CLASS,
       className,
diff --git a/ui/src/widgets/common.ts b/ui/src/widgets/common.ts
index 2498310..61352fa 100644
--- a/ui/src/widgets/common.ts
+++ b/ui/src/widgets/common.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assertUnreachable} from '../base/logging';
+
 // This file contains interfaces for attributes for various HTML elements.
 // They are typically used by widgets which pass attributes down to their
 // internal child, to provide a type-safe interface to users of those widgets.
@@ -59,3 +61,19 @@
 export interface HTMLLabelAttrs extends HTMLAttrs {
   for?: string;
 }
+
+export enum Intent {
+  None = 'none',
+  Primary = 'primary',
+}
+
+export function classForIntent(intent: Intent): string | undefined {
+  switch (intent) {
+    case Intent.None:
+      return undefined;
+    case Intent.Primary:
+      return 'pf-intent-primary';
+    default:
+      return assertUnreachable(intent);
+  }
+}
diff --git a/ui/src/widgets/details_shell.ts b/ui/src/widgets/details_shell.ts
index 159d016..e323d37 100644
--- a/ui/src/widgets/details_shell.ts
+++ b/ui/src/widgets/details_shell.ts
@@ -20,7 +20,8 @@
   title: m.Children;
   description?: m.Children;
   buttons?: m.Children;
-  // Stretch/shrink the content to fill the parent vertically.
+
+  // Vertically fill parent container and disable scrolling
   fillParent?: boolean;
 }
 
diff --git a/ui/src/widgets/form.ts b/ui/src/widgets/form.ts
index fbfaae6..8105f6d 100644
--- a/ui/src/widgets/form.ts
+++ b/ui/src/widgets/form.ts
@@ -17,6 +17,7 @@
 import {Button} from './button';
 import {HTMLAttrs, HTMLLabelAttrs} from './common';
 import {Popup} from './popup';
+import {Intent} from '../widgets/common';
 
 export interface FormAttrs extends HTMLAttrs {
   // Text to show on the "submit" button.
@@ -73,6 +74,7 @@
           label: submitLabel,
           rightIcon: submitIcon,
           className: Popup.DISMISS_POPUP_GROUP_CLASS,
+          intent: Intent.Primary,
           onclick: (e: Event) => {
             preventDefault && e.preventDefault();
             onSubmit();
@@ -84,13 +86,11 @@
             type: 'button',
             label: cancelLabel,
             className: Popup.DISMISS_POPUP_GROUP_CLASS,
-            minimal: true,
           }),
         // This reset button just clears the form.
         resetLabel &&
           m(Button, {
             label: resetLabel,
-            minimal: true,
             type: 'reset',
           }),
       ),
diff --git a/ui/src/widgets/multiselect.ts b/ui/src/widgets/multiselect.ts
index edff807..9756dc7 100644
--- a/ui/src/widgets/multiselect.ts
+++ b/ui/src/widgets/multiselect.ts
@@ -22,6 +22,7 @@
 import {Popup, PopupPosition} from './popup';
 import {scheduleFullRedraw} from './raf';
 import {TextInput} from './text_input';
+import {Intent} from './common';
 
 export interface Option {
   // The ID is used to indentify this option, and is used in callbacks.
@@ -46,7 +47,7 @@
 }
 
 export type PopupMultiSelectAttrs = MultiSelectAttrs & {
-  minimal?: boolean;
+  intent?: Intent;
   compact?: boolean;
   icon?: string;
   label: string;
@@ -106,7 +107,6 @@
                   label:
                     this.searchText === '' ? 'Clear All' : 'Clear Filtered',
                   icon: Icons.Deselect,
-                  minimal: true,
                   onclick: () => {
                     const diffs = options
                       .filter(({checked}) => checked)
@@ -134,7 +134,6 @@
                 label:
                   this.searchText === '' ? 'Select All' : 'Select Filtered',
                 icon: Icons.SelectAll,
-                minimal: true,
                 compact: true,
                 onclick: () => {
                   const diffs = options
@@ -148,7 +147,6 @@
               m(Button, {
                 label: this.searchText === '' ? 'Clear All' : 'Clear Filtered',
                 icon: Icons.Deselect,
-                minimal: true,
                 compact: true,
                 onclick: () => {
                   const diffs = options
@@ -193,7 +191,6 @@
         },
         label: '',
         icon: 'close',
-        minimal: true,
       });
     } else {
       return null;
@@ -225,7 +222,7 @@
   implements m.ClassComponent<PopupMultiSelectAttrs>
 {
   view({attrs}: m.CVnode<PopupMultiSelectAttrs>) {
-    const {icon, popupPosition = PopupPosition.Auto, minimal, compact} = attrs;
+    const {icon, popupPosition = PopupPosition.Auto, intent, compact} = attrs;
 
     return m(
       Popup,
@@ -233,7 +230,7 @@
         trigger: m(Button, {
           label: this.labelText(attrs),
           icon,
-          minimal,
+          intent,
           compact,
         }),
         position: popupPosition,
diff --git a/ui/src/widgets/virtual_scroll_container.ts b/ui/src/widgets/virtual_scroll_container.ts
deleted file mode 100644
index f6bd052..0000000
--- a/ui/src/widgets/virtual_scroll_container.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2023 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 m from 'mithril';
-
-import {findRef, toHTMLElement} from '../base/dom_utils';
-
-interface VirtualScrollContainerAttrs {
-  // Called when the scrolling element is created, updates, or scrolls.
-  onScroll?: (dom: HTMLElement) => void;
-}
-
-export class VirtualScrollContainer
-  implements m.ClassComponent<VirtualScrollContainerAttrs>
-{
-  private readonly REF = 'virtual-scroll-container';
-  view({attrs, children}: m.Vnode<VirtualScrollContainerAttrs>) {
-    const {onScroll = () => {}} = attrs;
-
-    return m(
-      '.pf-virtual-scroll-container',
-      {
-        ref: this.REF,
-        onscroll: (e: Event) => onScroll(e.target as HTMLElement),
-      },
-      children,
-    );
-  }
-
-  oncreate({dom, attrs}: m.VnodeDOM<VirtualScrollContainerAttrs, this>) {
-    const {onScroll = () => {}} = attrs;
-
-    const element = findRef(dom, this.REF);
-    if (element) {
-      onScroll(toHTMLElement(element));
-    }
-  }
-}
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
new file mode 100644
index 0000000..4fbe5c1
--- /dev/null
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -0,0 +1,150 @@
+// 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 {Trash} from '../base/disposable';
+import * as Geometry from '../base/geom';
+
+export interface VirtualScrollHelperOpts {
+  overdrawPx: number;
+
+  // How close we can get to undrawn regions before updating
+  tolerancePx: number;
+
+  callback: (r: Geometry.Rect) => void;
+}
+
+export interface Data {
+  opts: VirtualScrollHelperOpts;
+  rect?: Geometry.Rect;
+}
+
+export class VirtualScrollHelper {
+  private readonly _trash = new Trash();
+  private readonly _data: Data[] = [];
+
+  constructor(
+    sliderElement: HTMLElement,
+    containerElement: Element,
+    opts: VirtualScrollHelperOpts[] = [],
+  ) {
+    this._data = opts.map((opts) => {
+      return {opts};
+    });
+
+    const recalculateRects = () => {
+      this._data.forEach((data) =>
+        recalculatePuckRect(sliderElement, containerElement, data),
+      );
+    };
+
+    containerElement.addEventListener('scroll', recalculateRects, {
+      passive: true,
+    });
+    this._trash.addCallback(() =>
+      containerElement.removeEventListener('scroll', recalculateRects),
+    );
+
+    // Resize observer callbacks are called once immediately
+    const resizeObserver = new ResizeObserver(() => {
+      recalculateRects();
+    });
+
+    resizeObserver.observe(containerElement);
+    resizeObserver.observe(sliderElement);
+    this._trash.addCallback(() => {
+      resizeObserver.disconnect();
+    });
+  }
+
+  dispose() {
+    this._trash.dispose();
+  }
+}
+
+function recalculatePuckRect(
+  sliderElement: HTMLElement,
+  containerElement: Element,
+  data: Data,
+): void {
+  const {tolerancePx, overdrawPx, callback} = data.opts;
+  if (!data.rect) {
+    const targetPuckRect = getTargetPuckRect(
+      sliderElement,
+      containerElement,
+      overdrawPx,
+    );
+    callback(targetPuckRect);
+    data.rect = targetPuckRect;
+  } else {
+    const viewportRect = containerElement.getBoundingClientRect();
+
+    // Expand the viewportRect by the tolerance
+    const viewportExpandedRect = Geometry.expandRect(viewportRect, tolerancePx);
+
+    const sliderClientRect = sliderElement.getBoundingClientRect();
+    const viewportClamped = Geometry.intersectRects(
+      viewportExpandedRect,
+      sliderClientRect,
+    );
+
+    // Translate the puck rect into client space (currently in slider space)
+    const puckClientRect = Geometry.translateRect(data.rect, {
+      x: sliderClientRect.x,
+      y: sliderClientRect.y,
+    });
+
+    // Check if the tolerance rect entirely contains the expanded viewport rect
+    // If not, request an update
+    if (!Geometry.containsRect(puckClientRect, viewportClamped)) {
+      const targetPuckRect = getTargetPuckRect(
+        sliderElement,
+        containerElement,
+        overdrawPx,
+      );
+      callback(targetPuckRect);
+      data.rect = targetPuckRect;
+    }
+  }
+}
+
+// Returns what the puck rect should look like
+function getTargetPuckRect(
+  sliderElement: HTMLElement,
+  containerElement: Element,
+  overdrawPx: number,
+) {
+  const sliderElementRect = sliderElement.getBoundingClientRect();
+  const containerRect = containerElement.getBoundingClientRect();
+
+  // Calculate the intersection of the container's viewport and the target
+  const intersection = Geometry.intersectRects(
+    containerRect,
+    sliderElementRect,
+  );
+
+  // Pad the intersection by the overdraw amount
+  const intersectionExpanded = Geometry.expandRect(intersection, overdrawPx);
+
+  // Intersect with the original target rect unless we want to avoid resizes
+  const targetRect = Geometry.intersectRects(
+    intersectionExpanded,
+    sliderElementRect,
+  );
+
+  return Geometry.rebaseRect(
+    targetRect,
+    sliderElementRect.x,
+    sliderElementRect.y,
+  );
+}
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
new file mode 100644
index 0000000..0b97b96
--- /dev/null
+++ b/ui/src/widgets/virtual_table.ts
@@ -0,0 +1,262 @@
+// 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 m from 'mithril';
+import {Trash} from '../base/disposable';
+import {findRef, toHTMLElement} from '../base/dom_utils';
+import {Rect} from '../base/geom';
+import {assertExists} from '../base/logging';
+import {Style} from './common';
+import {scheduleFullRedraw} from './raf';
+import {VirtualScrollHelper} from './virtual_scroll_helper';
+
+/**
+ * The |VirtualTable| widget can be useful when attempting to render a large
+ * amount of tabular data - i.e. dumping the entire contents of a database
+ * table.
+ *
+ * A naive approach would be to load the entire dataset from the table and
+ * render it into the DOM. However, this has a number of disadvantages:
+ * - The query could potentially be very slow on large enough datasets.
+ * - The amount of data pulled could be larger than the available memory.
+ * - Rendering thousands of DOM elements using Mithril can get be slow.
+ * - Asking the browser to create and update thousands of elements on the DOM
+ *   can also be slow.
+ *
+ * This implementation takes advantage of the fact that computer monitors are
+ * only so tall, so most will only be able to display a small subset of rows at
+ * a given time, and the user will have to scroll to reveal more data.
+ *
+ * Thus, this widgets operates in such a way as to only render the DOM elements
+ * that are visible within the given scrolling container's viewport. To avoid
+ * spamming render updates, we render a few more rows above and below the
+ * current viewport, and only trigger an update once the user scrolls too close
+ * to the edge of the rendered data. These margins and tolerances are
+ * configurable with the |renderOverdrawPx| and |renderTolerancePx| attributes.
+ *
+ * When it comes to loading data, it's often more performant to run fewer large
+ * queries compared to more frequent smaller queries. Running a new query every
+ * time we want to update the DOM is usually too frequent, and results in
+ * flickering as the data is usually not loaded at the time the relevant row
+ * scrolls into view.
+ *
+ * Thus, this implementation employs two sets of limits, one to refresh the DOM
+ * and one larger one to re-query the data. The latter may be configured using
+ * the |queryOverdrawPx| and |queryTolerancePx| attributes.
+ *
+ * The smaller DOM refreshes and handled internally, but the user must be called
+ * to invoke a new query update. When new data is required, the |onReload|
+ * callback is called with the row offset and count.
+ *
+ * The data must be passed in the |data| attribute which contains the offset of
+ * the currently loaded data and a number of rows.
+ *
+ * Row and column content is flexible as m.Children are accepted and passed
+ * straight to mithril.
+ *
+ * The widget is quite opinionated in terms of its styling, but the entire
+ * widget and each row may be tweaked using |className| and |style| attributes
+ * which behave in the same way as they do on other Mithril components.
+ */
+
+export interface VirtualTableAttrs {
+  // A list of columns containing the header row content and column widths
+  columns: VirtualTableColumn[];
+
+  // Row height in px (each row must have the same height)
+  rowHeight: number;
+
+  // Offset of the first row
+  firstRowOffset: number;
+
+  // Total number of rows
+  numRows: number;
+
+  // The row data to render
+  rows: VirtualTableRow[];
+
+  // Optional: Called when we need to reload data
+  onReload?: (rowOffset: number, rowCount: number) => void;
+
+  // Additional class name applied to the table container element
+  className?: string;
+
+  // Additional styles applied to the table container element
+  style?: Style;
+
+  // Optional: Called when a row is hovered, passing the hovered row's id
+  onRowHover?: (id: number) => void;
+
+  // Optional: Called when a row is un-hovered, passing the un-hovered row's id
+  onRowOut?: (id: number) => void;
+
+  // Optional: Number of pixels equivalent of rows to overdraw above and below
+  // the viewport
+  // Defaults to a sensible value
+  renderOverdrawPx?: number;
+
+  // Optional: How close we can get to the edge before triggering a DOM redraw
+  // Defaults to a sensible value
+  renderTolerancePx?: number;
+
+  // Optional: Number of pixels equivalent of rows to query above and below the
+  // viewport
+  // Defaults to a sensible value
+  queryOverdrawPx?: number;
+
+  // Optional: How close we can get to the edge if the loaded data before we
+  // trigger another query
+  // Defaults to a sensible value
+  queryTolerancePx?: number;
+}
+
+export interface VirtualTableColumn {
+  // Content to render in the header row
+  header: m.Children;
+
+  // CSS width e.g. 12px, 4em, etc...
+  width: string;
+}
+
+export interface VirtualTableRow {
+  // Id for this row (must be unique within this dataset)
+  // Used for callbacks and as a Mithril key.
+  id: number;
+
+  // Data for each column in this row - must match number of elements in columns
+  cells: m.Children[];
+
+  // Optional: Additional class name applied to the row element
+  className?: string;
+}
+
+export class VirtualTable implements m.ClassComponent<VirtualTableAttrs> {
+  private readonly CONTAINER_REF = 'CONTAINER';
+  private readonly SLIDER_REF = 'SLIDER';
+  private readonly trash = new Trash();
+  private renderBounds = {rowStart: 0, rowEnd: 0};
+
+  view({attrs}: m.Vnode<VirtualTableAttrs>): m.Children {
+    const {columns, className, numRows, rowHeight, style} = attrs;
+    return m(
+      '.pf-vtable',
+      {className, style, ref: this.CONTAINER_REF},
+      m(
+        '.pf-vtable-content',
+        m(
+          '.pf-vtable-header',
+          columns.map((col) =>
+            m('.pf-vtable-data', {style: {width: col.width}}, col.header),
+          ),
+        ),
+        m(
+          '.pf-vtable-slider',
+          {ref: this.SLIDER_REF, style: {height: `${rowHeight * numRows}px`}},
+          m(
+            '.pf-vtable-puck',
+            {
+              style: {
+                transform: `translateY(${
+                  this.renderBounds.rowStart * rowHeight
+                }px)`,
+              },
+            },
+            this.renderContent(attrs),
+          ),
+        ),
+      ),
+    );
+  }
+
+  private renderContent(attrs: VirtualTableAttrs): m.Children {
+    const rows: m.ChildArray = [];
+    for (
+      let i = this.renderBounds.rowStart;
+      i < this.renderBounds.rowEnd;
+      ++i
+    ) {
+      rows.push(this.renderRow(attrs, i));
+    }
+    return rows;
+  }
+
+  private renderRow(attrs: VirtualTableAttrs, i: number): m.Children {
+    const {rows, firstRowOffset, rowHeight, columns, onRowHover, onRowOut} =
+      attrs;
+    if (i >= firstRowOffset && i < firstRowOffset + rows.length) {
+      // Render the row...
+      const index = i - firstRowOffset;
+      const rowData = rows[index];
+      return m(
+        '.pf-vtable-row',
+        {
+          className: rowData.className,
+          style: {height: `${rowHeight}px`},
+          onmouseover: () => {
+            onRowHover?.(rowData.id);
+          },
+          onmouseout: () => {
+            onRowOut?.(rowData.id);
+          },
+        },
+        rowData.cells.map((data, colIndex) =>
+          m('.pf-vtable-data', {style: {width: columns[colIndex].width}}, data),
+        ),
+      );
+    } else {
+      // Render a placeholder div with the same height as a row but a
+      // transparent background
+      return m('', {style: {height: `${rowHeight}px`}});
+    }
+  }
+
+  oncreate({dom, attrs}: m.VnodeDOM<VirtualTableAttrs>) {
+    const {
+      renderOverdrawPx = 200,
+      renderTolerancePx = 100,
+      queryOverdrawPx = 10_000,
+      queryTolerancePx = 5_000,
+    } = attrs;
+
+    const sliderEl = toHTMLElement(assertExists(findRef(dom, this.SLIDER_REF)));
+    const containerEl = assertExists(findRef(dom, this.CONTAINER_REF));
+    const virtualScrollHelper = new VirtualScrollHelper(sliderEl, containerEl, [
+      {
+        overdrawPx: renderOverdrawPx,
+        tolerancePx: renderTolerancePx,
+        callback: ({top, bottom}: Rect) => {
+          const height = bottom - top;
+          const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
+          const rowCount = Math.ceil(height / attrs.rowHeight / 2) * 2;
+          this.renderBounds = {rowStart, rowEnd: rowStart + rowCount};
+          scheduleFullRedraw();
+        },
+      },
+      {
+        overdrawPx: queryOverdrawPx,
+        tolerancePx: queryTolerancePx,
+        callback: ({top, bottom}: Rect) => {
+          const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
+          const rowEnd = Math.ceil(bottom / attrs.rowHeight);
+          attrs.onReload?.(rowStart, rowEnd - rowStart);
+        },
+      },
+    ]);
+    this.trash.add(virtualScrollHelper);
+  }
+
+  onremove(_: m.VnodeDOM<VirtualTableAttrs>) {
+    this.trash.dispose();
+  }
+}