Snap for 12901447 from e324242074e2e64a65e90a2933afd3ca4413554f to simpleperf-release

Change-Id: I038cae4aab50b05ea13108bfc17f216b0e5b973f
diff --git a/Android.bp b/Android.bp
index 77dc23c..84c9377 100644
--- a/Android.bp
+++ b/Android.bp
@@ -844,7 +844,6 @@
         ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
-        ":perfetto_src_tracing_platform_impl",
         ":perfetto_src_tracing_service_service",
         ":perfetto_src_tracing_system_backend",
     ],
@@ -1019,7 +1018,6 @@
         ":perfetto_src_tracing_ipc_default_socket",
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_service_service",
-        ":perfetto_src_tracing_platform_impl",
         ":perfetto_src_tracing_service_service",
         ":perfetto_src_tracing_system_backend",
     ],
@@ -2531,6 +2529,7 @@
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
         ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_macros_internal",
         ":perfetto_src_trace_processor_tables_tables",
         ":perfetto_src_trace_processor_types_types",
         ":perfetto_src_trace_processor_util_build_id",
@@ -2588,7 +2587,6 @@
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_producer_relay",
         ":perfetto_src_tracing_ipc_service_service",
-        ":perfetto_src_tracing_platform_impl",
         ":perfetto_src_tracing_service_service",
         ":perfetto_src_tracing_system_backend",
         ":perfetto_src_tracing_test_api_test_support",
@@ -2737,7 +2735,6 @@
         "perfetto_src_base_version_gen_h",
         "perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-        "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -2775,7 +2772,9 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_anomaly_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
+        "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5421,7 +5420,9 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_anomaly_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
+        "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5519,7 +5520,9 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_anomaly_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
+        "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5599,7 +5602,9 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_anomaly_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
+        "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5722,6 +5727,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -5751,6 +5757,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/android_log.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.gen.cc",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/camera_event.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.gen.cc",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.gen.cc",
@@ -5780,6 +5787,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.gen.h",
         "external/perfetto/protos/perfetto/trace/android/android_log.gen.h",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.gen.h",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.gen.h",
         "external/perfetto/protos/perfetto/trace/android/camera_event.gen.h",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.gen.h",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.gen.h",
@@ -5802,6 +5810,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -5830,6 +5839,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/android_log.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.pb.cc",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/camera_event.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pb.cc",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pb.cc",
@@ -5858,6 +5868,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pb.h",
         "external/perfetto/protos/perfetto/trace/android/android_log.pb.h",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.pb.h",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pb.h",
         "external/perfetto/protos/perfetto/trace/android/camera_event.pb.h",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pb.h",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pb.h",
@@ -6452,6 +6463,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -6481,6 +6493,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/android_log.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/camera_event.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pbzero.cc",
@@ -6510,6 +6523,7 @@
         "external/perfetto/protos/perfetto/trace/android/android_game_intervention_list.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/android_log.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/android_system_property.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/android/bluetooth_trace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/camera_event.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/frame_timeline_event.pbzero.h",
         "external/perfetto/protos/perfetto/trace/android/gpu_mem_event.pbzero.h",
@@ -6753,6 +6767,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -12807,10 +12822,12 @@
         "src/trace_processor/importers/proto/graphics_frame_event_parser.cc",
         "src/trace_processor/importers/proto/heap_graph_module.cc",
         "src/trace_processor/importers/proto/heap_graph_tracker.cc",
+        "src/trace_processor/importers/proto/jit_tracker.cc",
         "src/trace_processor/importers/proto/metadata_module.cc",
         "src/trace_processor/importers/proto/pigweed_detokenizer.cc",
         "src/trace_processor/importers/proto/pixel_modem_module.cc",
         "src/trace_processor/importers/proto/pixel_modem_parser.cc",
+        "src/trace_processor/importers/proto/profile_module.cc",
         "src/trace_processor/importers/proto/statsd_module.cc",
         "src/trace_processor/importers/proto/string_encoding_utils.cc",
         "src/trace_processor/importers/proto/system_probes_module.cc",
@@ -12853,21 +12870,6 @@
     ],
 }
 
-// GN: //src/trace_processor/importers/proto:gen_cc_config_descriptor
-genrule {
-    name: "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
-    srcs: [
-        ":perfetto_protos_perfetto_config_descriptor",
-    ],
-    cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
-    out: [
-        "src/trace_processor/importers/proto/config.descriptor.h",
-    ],
-    tool_files: [
-        "tools/gen_cc_proto_descriptor.py",
-    ],
-}
-
 // GN: //src/trace_processor/importers/proto:gen_cc_statsd_atoms_descriptor
 genrule {
     name: "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
@@ -12923,7 +12925,6 @@
         "src/trace_processor/importers/proto/chrome_system_probes_module.cc",
         "src/trace_processor/importers/proto/chrome_system_probes_parser.cc",
         "src/trace_processor/importers/proto/default_modules.cc",
-        "src/trace_processor/importers/proto/jit_tracker.cc",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_module.cc",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc",
         "src/trace_processor/importers/proto/metadata_minimal_module.cc",
@@ -12932,7 +12933,6 @@
         "src/trace_processor/importers/proto/packet_analyzer.cc",
         "src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
         "src/trace_processor/importers/proto/perf_sample_tracker.cc",
-        "src/trace_processor/importers/proto/profile_module.cc",
         "src/trace_processor/importers/proto/profile_packet_sequence_state.cc",
         "src/trace_processor/importers/proto/profile_packet_utils.cc",
         "src/trace_processor/importers/proto/proto_trace_parser_impl.cc",
@@ -13139,6 +13139,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
@@ -13471,10 +13472,8 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc",
@@ -13532,7 +13531,6 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor_unittest.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/connected_flow_unittest.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant_unittest.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice_unittest.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout_unittest.cc",
     ],
@@ -13654,6 +13652,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/winscope/inputmethod.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/winscope/viewcapture.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql",
+        "src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql",
         "src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/**/*.sql",
         "src/trace_processor/perfetto_sql/stdlib/counters/global_tracks.sql",
@@ -13719,11 +13718,12 @@
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads_w_processes.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/trace.sql",
-        "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/threads.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_freq_idle.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
@@ -13869,6 +13869,14 @@
     ],
 }
 
+// GN: //src/trace_processor/tables:macros_internal
+filegroup {
+    name: "perfetto_src_trace_processor_tables_macros_internal",
+    srcs: [
+        "src/trace_processor/tables/macros_internal.cc",
+    ],
+}
+
 // GN: //src/trace_processor/tables:py_tables_unittest
 genrule {
     name: "perfetto_src_trace_processor_tables_py_tables_unittest",
@@ -13901,7 +13909,6 @@
 filegroup {
     name: "perfetto_src_trace_processor_tables_tables",
     srcs: [
-        "src/trace_processor/tables/macros_internal.cc",
         "src/trace_processor/tables/table_destructors.cc",
     ],
 }
@@ -14836,6 +14843,8 @@
         "src/tracing/internal/track_event_internal.cc",
         "src/tracing/internal/track_event_interned_fields.cc",
         "src/tracing/platform.cc",
+        "src/tracing/platform_posix.cc",
+        "src/tracing/platform_windows.cc",
         "src/tracing/traced_value.cc",
         "src/tracing/tracing.cc",
         "src/tracing/tracing_policy.cc",
@@ -14969,15 +14978,6 @@
     ],
 }
 
-// GN: //src/tracing:platform_impl
-filegroup {
-    name: "perfetto_src_tracing_platform_impl",
-    srcs: [
-        "src/tracing/platform_posix.cc",
-        "src/tracing/platform_windows.cc",
-    ],
-}
-
 // GN: //src/tracing/service:service
 filegroup {
     name: "perfetto_src_tracing_service_service",
@@ -15164,6 +15164,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -15783,6 +15784,7 @@
         ":perfetto_src_trace_processor_sqlite_unittests",
         ":perfetto_src_trace_processor_storage_minimal",
         ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_macros_internal",
         ":perfetto_src_trace_processor_tables_tables",
         ":perfetto_src_trace_processor_tables_unittests",
         ":perfetto_src_trace_processor_top_level_unittests",
@@ -15866,7 +15868,6 @@
         ":perfetto_src_tracing_ipc_producer_producer",
         ":perfetto_src_tracing_ipc_producer_relay",
         ":perfetto_src_tracing_ipc_unittests",
-        ":perfetto_src_tracing_platform_impl",
         ":perfetto_src_tracing_service_service",
         ":perfetto_src_tracing_service_unittests",
         ":perfetto_src_tracing_service_zlib_compressor",
@@ -16025,7 +16026,6 @@
         "perfetto_src_trace_processor_gen_cc_test_messages_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-        "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -16470,6 +16470,7 @@
         "protos/perfetto/trace/android/android_system_property.proto",
         "protos/perfetto/trace/android/app/statusbarmanager.proto",
         "protos/perfetto/trace/android/app/window_configuration.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/content/activityinfo.proto",
         "protos/perfetto/trace/android/content/configuration.proto",
@@ -16843,6 +16844,7 @@
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
         ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_macros_internal",
         ":perfetto_src_trace_processor_tables_tables",
         ":perfetto_src_trace_processor_types_types",
         ":perfetto_src_trace_processor_util_build_id",
@@ -16914,7 +16916,6 @@
         "perfetto_src_base_version_gen_h",
         "perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-        "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -17057,6 +17058,7 @@
         ":perfetto_src_trace_processor_sorter_sorter",
         ":perfetto_src_trace_processor_storage_minimal",
         ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_macros_internal",
         ":perfetto_src_trace_processor_tables_tables",
         ":perfetto_src_trace_processor_types_types",
         ":perfetto_src_trace_processor_util_build_id",
@@ -17270,6 +17272,7 @@
         ":perfetto_src_trace_processor_sqlite_sqlite",
         ":perfetto_src_trace_processor_storage_minimal",
         ":perfetto_src_trace_processor_storage_storage",
+        ":perfetto_src_trace_processor_tables_macros_internal",
         ":perfetto_src_trace_processor_tables_tables",
         ":perfetto_src_trace_processor_types_types",
         ":perfetto_src_trace_processor_util_build_id",
@@ -17347,7 +17350,6 @@
         "perfetto_src_base_version_gen_h",
         "perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-        "perfetto_src_trace_processor_importers_proto_gen_cc_config_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_trace_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
diff --git a/BUILD b/BUILD
index 0af9520..e9e13ab 100644
--- a/BUILD
+++ b/BUILD
@@ -173,7 +173,6 @@
         ":src_tracing_ipc_default_socket",
         ":src_tracing_ipc_producer_producer",
         ":src_tracing_ipc_service_service",
-        ":src_tracing_platform_impl",
         ":src_tracing_service_service",
         ":src_tracing_system_backend",
     ],
@@ -392,6 +391,7 @@
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
         ":src_trace_processor_storage_storage",
+        ":src_trace_processor_tables_macros_internal",
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_tables_tables_python",
         ":src_trace_processor_types_types",
@@ -493,7 +493,6 @@
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -1888,6 +1887,7 @@
         "src/trace_processor/importers/etm/mapping_version.cc",
         "src/trace_processor/importers/etm/mapping_version.h",
         "src/trace_processor/importers/etm/opencsd.h",
+        "src/trace_processor/importers/etm/sql_values.h",
         "src/trace_processor/importers/etm/storage_handle.cc",
         "src/trace_processor/importers/etm/storage_handle.h",
         "src/trace_processor/importers/etm/target_memory.cc",
@@ -2272,6 +2272,8 @@
         "src/trace_processor/importers/proto/heap_graph_module.h",
         "src/trace_processor/importers/proto/heap_graph_tracker.cc",
         "src/trace_processor/importers/proto/heap_graph_tracker.h",
+        "src/trace_processor/importers/proto/jit_tracker.cc",
+        "src/trace_processor/importers/proto/jit_tracker.h",
         "src/trace_processor/importers/proto/metadata_module.cc",
         "src/trace_processor/importers/proto/metadata_module.h",
         "src/trace_processor/importers/proto/pigweed_detokenizer.cc",
@@ -2280,6 +2282,8 @@
         "src/trace_processor/importers/proto/pixel_modem_module.h",
         "src/trace_processor/importers/proto/pixel_modem_parser.cc",
         "src/trace_processor/importers/proto/pixel_modem_parser.h",
+        "src/trace_processor/importers/proto/profile_module.cc",
+        "src/trace_processor/importers/proto/profile_module.h",
         "src/trace_processor/importers/proto/statsd_module.cc",
         "src/trace_processor/importers/proto/statsd_module.h",
         "src/trace_processor/importers/proto/string_encoding_utils.cc",
@@ -2323,17 +2327,6 @@
     ],
 )
 
-# GN target: //src/trace_processor/importers/proto:gen_cc_config_descriptor
-perfetto_cc_proto_descriptor(
-    name = "src_trace_processor_importers_proto_gen_cc_config_descriptor",
-    deps = [
-        ":protos_perfetto_config_descriptor",
-    ],
-    outs = [
-        "src/trace_processor/importers/proto/config.descriptor.h",
-    ],
-)
-
 # GN target: //src/trace_processor/importers/proto:gen_cc_statsd_atoms_descriptor
 perfetto_cc_proto_descriptor(
     name = "src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
@@ -2383,8 +2376,6 @@
         "src/trace_processor/importers/proto/chrome_system_probes_parser.h",
         "src/trace_processor/importers/proto/default_modules.cc",
         "src/trace_processor/importers/proto/default_modules.h",
-        "src/trace_processor/importers/proto/jit_tracker.cc",
-        "src/trace_processor/importers/proto/jit_tracker.h",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_module.cc",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_module.h",
         "src/trace_processor/importers/proto/memory_tracker_snapshot_parser.cc",
@@ -2401,8 +2392,6 @@
         "src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
         "src/trace_processor/importers/proto/perf_sample_tracker.cc",
         "src/trace_processor/importers/proto/perf_sample_tracker.h",
-        "src/trace_processor/importers/proto/profile_module.cc",
-        "src/trace_processor/importers/proto/profile_module.h",
         "src/trace_processor/importers/proto/profile_packet_sequence_state.cc",
         "src/trace_processor/importers/proto/profile_packet_sequence_state.h",
         "src/trace_processor/importers/proto/profile_packet_utils.cc",
@@ -2495,6 +2484,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_boot_unagg.sql",
@@ -2870,6 +2860,7 @@
     name = "src_trace_processor_perfetto_sql_intrinsics_operators_etm_hdr",
     srcs = [
         "src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.h",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h",
     ],
 )
 
@@ -2878,6 +2869,7 @@
     name = "src_trace_processor_perfetto_sql_intrinsics_operators_etm_impl",
     srcs = [
         "src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc",
+        "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc",
     ],
 )
 
@@ -2919,14 +2911,10 @@
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.h",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.h",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc",
-        "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.cc",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.h",
         "src/trace_processor/perfetto_sql/intrinsics/table_functions/flamegraph_construction_algorithms.cc",
@@ -3134,6 +3122,19 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/appleos/instruments:instruments
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_appleos_instruments_instruments",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql",
+    ],
+)
+
+# GN target: //src/trace_processor/perfetto_sql/stdlib/appleos:appleos
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_appleos_appleos",
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/callstacks:callstacks
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
@@ -3351,7 +3352,7 @@
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/threads_w_processes.sql",
         "src/trace_processor/perfetto_sql/stdlib/viz/summary/trace.sql",
-        "src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql",
+        "src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql",
     ],
 )
 
@@ -3372,6 +3373,7 @@
         "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_freq_idle.sql",
+        "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/cpu_split.sql",
         "src/trace_processor/perfetto_sql/stdlib/wattson/curves/device.sql",
@@ -3400,6 +3402,8 @@
         ":src_trace_processor_perfetto_sql_stdlib_android_memory_memory",
         ":src_trace_processor_perfetto_sql_stdlib_android_startup_startup",
         ":src_trace_processor_perfetto_sql_stdlib_android_winscope_winscope",
+        ":src_trace_processor_perfetto_sql_stdlib_appleos_appleos",
+        ":src_trace_processor_perfetto_sql_stdlib_appleos_instruments_instruments",
         ":src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
         ":src_trace_processor_perfetto_sql_stdlib_chrome_chrome_sql",
         ":src_trace_processor_perfetto_sql_stdlib_counters_counters",
@@ -3538,12 +3542,19 @@
     ],
 )
 
+# GN target: //src/trace_processor/tables:macros_internal
+perfetto_filegroup(
+    name = "src_trace_processor_tables_macros_internal",
+    srcs = [
+        "src/trace_processor/tables/macros_internal.cc",
+        "src/trace_processor/tables/macros_internal.h",
+    ],
+)
+
 # GN target: //src/trace_processor/tables:tables
 perfetto_filegroup(
     name = "src_trace_processor_tables_tables",
     srcs = [
-        "src/trace_processor/tables/macros_internal.cc",
-        "src/trace_processor/tables/macros_internal.h",
         "src/trace_processor/tables/table_destructors.cc",
     ],
 )
@@ -4293,6 +4304,8 @@
         "src/tracing/internal/track_event_internal.cc",
         "src/tracing/internal/track_event_interned_fields.cc",
         "src/tracing/platform.cc",
+        "src/tracing/platform_posix.cc",
+        "src/tracing/platform_windows.cc",
         "src/tracing/traced_value.cc",
         "src/tracing/tracing.cc",
         "src/tracing/tracing_policy.cc",
@@ -4320,15 +4333,6 @@
     ],
 )
 
-# GN target: //src/tracing:platform_impl
-perfetto_filegroup(
-    name = "src_tracing_platform_impl",
-    srcs = [
-        "src/tracing/platform_posix.cc",
-        "src/tracing/platform_windows.cc",
-    ],
-)
-
 # GN target: //src/tracing:system_backend
 perfetto_filegroup(
     name = "src_tracing_system_backend",
@@ -5276,7 +5280,9 @@
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_anomaly_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
+        "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
@@ -5500,6 +5506,7 @@
         "protos/perfetto/trace/android/android_game_intervention_list.proto",
         "protos/perfetto/trace/android/android_log.proto",
         "protos/perfetto/trace/android/android_system_property.proto",
+        "protos/perfetto/trace/android/bluetooth_trace.proto",
         "protos/perfetto/trace/android/camera_event.proto",
         "protos/perfetto/trace/android/frame_timeline_event.proto",
         "protos/perfetto/trace/android/gpu_mem_event.proto",
@@ -6524,7 +6531,6 @@
         ":src_tracing_ipc_default_socket",
         ":src_tracing_ipc_producer_producer",
         ":src_tracing_ipc_service_service",
-        ":src_tracing_platform_impl",
         ":src_tracing_service_service",
         ":src_tracing_system_backend",
     ],
@@ -6775,6 +6781,7 @@
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
         ":src_trace_processor_storage_storage",
+        ":src_trace_processor_tables_macros_internal",
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_tables_tables_python",
         ":src_trace_processor_types_types",
@@ -6876,7 +6883,6 @@
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -6994,6 +7000,7 @@
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
         ":src_trace_processor_storage_storage",
+        ":src_trace_processor_tables_macros_internal",
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_tables_tables_python",
         ":src_trace_processor_types_types",
@@ -7080,7 +7087,6 @@
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
@@ -7196,6 +7202,7 @@
         ":src_trace_processor_sqlite_sqlite",
         ":src_trace_processor_storage_minimal",
         ":src_trace_processor_storage_storage",
+        ":src_trace_processor_tables_macros_internal",
         ":src_trace_processor_tables_tables",
         ":src_trace_processor_tables_tables_python",
         ":src_trace_processor_types_types",
@@ -7284,7 +7291,6 @@
                ":src_trace_processor_containers_containers",
                ":src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
                ":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
diff --git a/BUILD.gn b/BUILD.gn
index b495c20..41d8014 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -283,7 +283,6 @@
     public_deps = [
       "gn:default_deps",
       "src/tracing:client_api",
-      "src/tracing:platform_impl",
     ]
     sources = [ "include/perfetto/tracing.h" ]
     assert_no_deps = [ "gn:protobuf_lite" ]
@@ -300,7 +299,6 @@
     deps = [
       "src/trace_processor/importers/memory_tracker:graph_processor",
       "src/tracing:client_api",
-      "src/tracing:platform_impl",
       "src/tracing/core",
     ]
     configs -= [ "//build/config/compiler:chromium_code" ]  # nogncheck
diff --git a/CHANGELOG b/CHANGELOG
index 567fc95..d61826e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,18 @@
 Unreleased:
   Tracing service and probes:
+    *
+  SQL Standard library:
+    *
+  Trace Processor:
+    *
+  UI:
+    *
+  SDK:
+    *
+
+
+v49.0 - 2025-01-06:
+  Tracing service and probes:
     * Add `--clone-by-name` to the perfetto command line. This allows cloning a
       tracing session by its unique_session_name.
     * Fixed a bug that would delay the trace start acknowledgement, resulting
@@ -46,11 +59,22 @@
       TIMESTAMP and DURATION refers to time columns in nanoseconds. ID column
       is a primary key for the column and JOINID is referencing ID
       column of other table.
-  Trace Processor:
-    *
+    * Removed the `experimental_sched_upid` table. Prefer the joining `sched`
+      and `thread` tables directly instead.
+    * Removed the experimental_counter_dur table. Prefer using the
+      `counter_leading_intervals` macro from the `counters.intervals` standard
+      library module.
   UI:
     * Introduced `Open table:` command which would open any Perfetto Standard
       Library table in a new tab.
+    * Fixed behaviour of sorting and nesting of track event tracks for counter
+      tracks, thread tracks and process tracks.
+    * Various improvements to timeline rendering performance.
+    * Added workspace switcher and menu to move tracks between workspaces.
+    * Completely overhauled recording page.
+    * Improved area selection UX.
+    * Improved fuzzy search.
+    * Hide 'Open with legacy UI' by default.
   SDK:
     * Added `NamedTrack`, it allows creating arbitrarily named tracks.
 
diff --git a/OWNERS b/OWNERS
index 1303764..4dca898 100644
--- a/OWNERS
+++ b/OWNERS
@@ -38,3 +38,4 @@
 eseckler@chromium.org
 nuskos@chromium.org
 skyostil@chromium.org
+include platform/system/core:main:/janitors/OWNERS #{LAST_RESORT_SUGGESTION}
diff --git a/docs/instrumentation/tracing-sdk.md b/docs/instrumentation/tracing-sdk.md
index a3b7c85a..f9164e0 100644
--- a/docs/instrumentation/tracing-sdk.md
+++ b/docs/instrumentation/tracing-sdk.md
@@ -29,7 +29,7 @@
 To start using the Client API, first check out the latest SDK release:
 
 ```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v49.0
 ```
 
 The SDK consists of two files, `sdk/perfetto.h` and `sdk/perfetto.cc`. These are
diff --git a/examples/sdk/README.md b/examples/sdk/README.md
index 042b7b5..3e7a88a 100644
--- a/examples/sdk/README.md
+++ b/examples/sdk/README.md
@@ -15,7 +15,7 @@
 First, check out the latest Perfetto release:
 
 ```bash
-git clone https://android.googlesource.com/platform/external/perfetto -b v48.1
+git clone https://android.googlesource.com/platform/external/perfetto -b v49.0
 ```
 
 Then, build using CMake:
diff --git a/gn/standalone/wasm_typescript_declaration.d.ts b/gn/standalone/wasm_typescript_declaration.d.ts
index 3f8547d..6e27ebb 100644
--- a/gn/standalone/wasm_typescript_declaration.d.ts
+++ b/gn/standalone/wasm_typescript_declaration.d.ts
@@ -64,5 +64,6 @@
     printErr(s: string): void;
     onRuntimeInitialized(): void;
     onAbort?(): void;
+    wasmBinary ?: ArrayBuffer;
   }
 }
diff --git a/include/perfetto/ext/trace_processor/export_json.h b/include/perfetto/ext/trace_processor/export_json.h
index 23119bf..f115a2f 100644
--- a/include/perfetto/ext/trace_processor/export_json.h
+++ b/include/perfetto/ext/trace_processor/export_json.h
@@ -44,12 +44,12 @@
   OutputWriter();
   virtual ~OutputWriter();
 
-  virtual util::Status AppendString(const std::string&) = 0;
+  virtual base::Status AppendString(const std::string&) = 0;
 };
 
 // Public for Chrome. Exports the trace loaded in TraceProcessorStorage to json,
 // applying argument, metadata and label filtering using the callbacks.
-util::Status PERFETTO_EXPORT_COMPONENT
+base::Status PERFETTO_EXPORT_COMPONENT
 ExportJson(TraceProcessorStorage*,
            OutputWriter*,
            ArgumentFilterPredicate = nullptr,
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h
index 716e73a..d572459 100644
--- a/include/perfetto/trace_processor/basic_types.h
+++ b/include/perfetto/trace_processor/basic_types.h
@@ -148,13 +148,17 @@
   SortingMode sorting_mode = SortingMode::kDefaultHeuristics;
 
   // When set to false, this option makes the trace processor not include ftrace
-  // events in the raw table; this makes converting events back to the systrace
-  // text format impossible. On the other hand, it also saves ~50% of memory
-  // usage of trace processor. For reference, Studio intends to use this option.
+  // events in the ftrace_event table; this makes converting events back to the
+  // systrace text format impossible. On the other hand, it also saves ~50% of
+  // memory usage of trace processor. For reference, Studio intends to use this
+  // option.
   //
-  // Note: "generic" ftrace events will be parsed into the raw table even if
-  // this flag is false and all other events which parse into the raw table are
-  // unaffected by this flag.
+  // Note: "generic" ftrace events will be parsed into the ftrace_event table
+  // even if this flag is false.
+  //
+  // Note: this option should really be named
+  // `ingest_ftrace_in_ftrace_event_table` as the use of the `raw` table is
+  // deprecated.
   bool ingest_ftrace_in_raw_table = true;
 
   // Indicates the event which should be used as a marker to drop ftrace data in
diff --git a/include/perfetto/trace_processor/iterator.h b/include/perfetto/trace_processor/iterator.h
index 6ab2442..ec41458 100644
--- a/include/perfetto/trace_processor/iterator.h
+++ b/include/perfetto/trace_processor/iterator.h
@@ -88,7 +88,7 @@
   std::string LastStatementSql();
 
   // Returns the status of the iterator.
-  util::Status Status();
+  base::Status Status();
 
  private:
   friend class QueryResultSerializer;
diff --git a/include/perfetto/trace_processor/read_trace.h b/include/perfetto/trace_processor/read_trace.h
index 1b8ab12..f60254c 100644
--- a/include/perfetto/trace_processor/read_trace.h
+++ b/include/perfetto/trace_processor/read_trace.h
@@ -29,13 +29,13 @@
 
 class TraceProcessor;
 
-util::Status PERFETTO_EXPORT_COMPONENT ReadTrace(
+base::Status PERFETTO_EXPORT_COMPONENT ReadTrace(
     TraceProcessor* tp,
     const char* filename,
     const std::function<void(uint64_t parsed_size)>& progress_callback =
         [](uint64_t) {});
 
-util::Status PERFETTO_EXPORT_COMPONENT
+base::Status PERFETTO_EXPORT_COMPONENT
 DecompressTrace(const uint8_t* data, size_t size, std::vector<uint8_t>* output);
 
 }  // namespace trace_processor
diff --git a/include/perfetto/trace_processor/trace_processor.h b/include/perfetto/trace_processor/trace_processor.h
index 1886ee2f..07c0a12 100644
--- a/include/perfetto/trace_processor/trace_processor.h
+++ b/include/perfetto/trace_processor/trace_processor.h
@@ -17,22 +17,21 @@
 #ifndef INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
 #define INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
 
+#include <cstddef>
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <vector>
 
-#include "perfetto/base/build_config.h"
 #include "perfetto/base/export.h"
 #include "perfetto/base/status.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/iterator.h"
 #include "perfetto/trace_processor/metatrace_config.h"
-#include "perfetto/trace_processor/status.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "perfetto/trace_processor/trace_processor_storage.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Extends TraceProcessorStorage to support execution of SQL queries on loaded
 // traces. See TraceProcessorStorage for parsing of trace files.
@@ -47,6 +46,10 @@
 
   ~TraceProcessor() override;
 
+  // =================================================================
+  // |        PerfettoSQL related functionality starts here          |
+  // =================================================================
+
   // Executes the SQL on the loaded portion of the trace.
   //
   // More than one SQL statement can be passed to this function; all but the
@@ -68,6 +71,69 @@
   // name.
   virtual base::Status RegisterSqlPackage(SqlPackage) = 0;
 
+  // Deprecated. Use |RegisterSqlPackage()| instead, which is identical in
+  // functionality to |RegisterSqlModule()| and the only difference is in
+  // the argument, which is directly translatable to |SqlPackage|.
+  virtual base::Status RegisterSqlModule(SqlModule) = 0;
+
+  // =================================================================
+  // |        Metatracing related functionality starts here          |
+  // =================================================================
+
+  // Enables "meta-tracing" of trace processor.
+  // Metatracing involves tracing trace processor itself to root-cause
+  // performace issues in trace processor. See |DisableAndReadMetatrace| for
+  // more information on the format of the metatrace.
+  using MetatraceConfig = metatrace::MetatraceConfig;
+  using MetatraceCategories = metatrace::MetatraceCategories;
+  virtual void EnableMetatrace(MetatraceConfig config = {}) = 0;
+
+  // Disables "meta-tracing" of trace processor and writes the trace as a
+  // sequence of |TracePackets| into |trace_proto| returning the status of this
+  // read.
+  virtual base::Status DisableAndReadMetatrace(
+      std::vector<uint8_t>* trace_proto) = 0;
+
+  // =================================================================
+  // |              Advanced functionality starts here               |
+  // =================================================================
+
+  // Sets/returns the name of the currently loaded trace or an empty string if
+  // no trace is fully loaded yet. This has no effect on the Trace Processor
+  // functionality and is used for UI purposes only.
+  // The returned name is NOT a path and will contain extra text w.r.t. the
+  // argument originally passed to SetCurrentTraceName(), e.g., "file (42 MB)".
+  virtual std::string GetCurrentTraceName() = 0;
+  virtual void SetCurrentTraceName(const std::string&) = 0;
+
+  // Registers the contents of a file.
+  // This method can be used to pass out of band data to the trace processor
+  // which can be used by importers to do some advanced processing. For example
+  // if you pass binaries these are used to decode ETM traces.
+  // Registering the same file twice will return an error.
+  virtual base::Status RegisterFileContent(const std::string& path,
+                                           TraceBlobView content) = 0;
+
+  // Interrupts the current query. Typically used by Ctrl-C handler.
+  virtual void InterruptQuery() = 0;
+
+  // Restores Trace Processor to its pristine state. It preserves the built-in
+  // tables/views/functions created by the ingestion process. Returns the number
+  // of objects created in runtime that has been deleted.
+  // NOTE: No Iterators can active when called.
+  virtual size_t RestoreInitialTables() = 0;
+
+  // =================================================================
+  // |  Trace-based metrics (v1) related functionality starts here   |
+  // =================================================================
+  //
+  // WARNING: The metrics v1 system is "soft" deprecated: no new metrics are
+  // allowed but we still fully support any existing metrics written using this
+  // system.
+  //
+  // If possible, prefer using the metrics v2 methods above for any new
+  // usecases.
+
   // Registers a metric at the given path which will run the specified SQL.
   virtual base::Status RegisterMetric(const std::string& path,
                                       const std::string& sql) = 0;
@@ -105,58 +171,13 @@
       MetricResultFormat format,
       std::string* metrics_string) = 0;
 
-  // Interrupts the current query. Typically used by Ctrl-C handler.
-  virtual void InterruptQuery() = 0;
-
-  // Restores Trace Processor to its pristine state. It preserves the built-in
-  // tables/views/functions created by the ingestion process. Returns the number
-  // of objects created in runtime that has been deleted.
-  // NOTE: No Iterators can active when called.
-  virtual size_t RestoreInitialTables() = 0;
-
-  // Sets/returns the name of the currently loaded trace or an empty string if
-  // no trace is fully loaded yet. This has no effect on the Trace Processor
-  // functionality and is used for UI purposes only.
-  // The returned name is NOT a path and will contain extra text w.r.t. the
-  // argument originally passed to SetCurrentTraceName(), e.g., "file (42 MB)".
-  virtual std::string GetCurrentTraceName() = 0;
-  virtual void SetCurrentTraceName(const std::string&) = 0;
-
-  // Enables "meta-tracing" of trace processor.
-  // Metatracing involves tracing trace processor itself to root-cause
-  // performace issues in trace processor. See |DisableAndReadMetatrace| for
-  // more information on the format of the metatrace.
-  using MetatraceConfig = metatrace::MetatraceConfig;
-  using MetatraceCategories = metatrace::MetatraceCategories;
-  virtual void EnableMetatrace(MetatraceConfig config = {}) = 0;
-
-  // Disables "meta-tracing" of trace processor and writes the trace as a
-  // sequence of |TracePackets| into |trace_proto| returning the status of this
-  // read.
-  virtual base::Status DisableAndReadMetatrace(
-      std::vector<uint8_t>* trace_proto) = 0;
-
   // Gets all the currently loaded proto descriptors used in metric computation.
   // This includes all compiled-in binary descriptors, and all proto descriptors
   // loaded by trace processor shell at runtime. The message is encoded as
   // DescriptorSet, defined in perfetto/trace_processor/trace_processor.proto.
   virtual std::vector<uint8_t> GetMetricDescriptors() = 0;
-
-  // Deprecated. Use |RegisterSqlPackage()| instead, which is identical in
-  // functionality to |RegisterSqlModule()| and the only difference is in
-  // the argument, which is directly translatable to |SqlPackage|.
-  virtual base::Status RegisterSqlModule(SqlModule) = 0;
-
-  // Registers the contents of a file.
-  // This method can be used to pass out of band data to the trace processor
-  // which can be used by importers to do some advanced processing. For example
-  // if you pass binaries these are used to decode ETM traces.
-  // Registering the same file twice will return an error.
-  virtual base::Status RegisterFileContent(const std::string& path,
-                                           TraceBlobView content) = 0;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // INCLUDE_PERFETTO_TRACE_PROCESSOR_TRACE_PROCESSOR_H_
diff --git a/include/perfetto/trace_processor/trace_processor_storage.h b/include/perfetto/trace_processor/trace_processor_storage.h
index 417ac72..a286873 100644
--- a/include/perfetto/trace_processor/trace_processor_storage.h
+++ b/include/perfetto/trace_processor/trace_processor_storage.h
@@ -44,11 +44,11 @@
   // status if some unrecoverable error happened. If this happens, the
   // TraceProcessor will ignore the following Parse() requests, drop data on the
   // floor and return errors forever.
-  virtual util::Status Parse(TraceBlobView) = 0;
+  virtual base::Status Parse(TraceBlobView) = 0;
 
   // Shorthand for Parse(TraceBlobView(TraceBlob(TakeOwnership(buf, size))).
   // For compatibility with older API clients.
-  util::Status Parse(std::unique_ptr<uint8_t[]> buf, size_t size);
+  base::Status Parse(std::unique_ptr<uint8_t[]> buf, size_t size);
 
   // Forces all data in the trace to be pushed to tables without buffering data
   // in sorting queues. This is useful if queries need to be performed to
diff --git a/include/perfetto/tracing/core/data_source_config.h b/include/perfetto/tracing/core/data_source_config.h
index 765a642..31f2d54 100644
--- a/include/perfetto/tracing/core/data_source_config.h
+++ b/include/perfetto/tracing/core/data_source_config.h
@@ -21,8 +21,8 @@
 // using ::perfetto::Foo = ::perfetto::protos::gen::Foo.
 // See comments in forward_decls.h for the historical reasons of this
 // indirection layer.
-#include "perfetto/tracing/core/forward_decls.h"
+#include "perfetto/tracing/core/forward_decls.h"  // IWYU pragma: export
 
-#include "protos/perfetto/config/data_source_config.gen.h"
+#include "protos/perfetto/config/data_source_config.gen.h"  // IWYU pragma: export
 
 #endif  // INCLUDE_PERFETTO_TRACING_CORE_DATA_SOURCE_CONFIG_H_
diff --git a/include/perfetto/tracing/internal/data_source_internal.h b/include/perfetto/tracing/internal/data_source_internal.h
index 493098a..c81ff0d 100644
--- a/include/perfetto/tracing/internal/data_source_internal.h
+++ b/include/perfetto/tracing/internal/data_source_internal.h
@@ -22,12 +22,10 @@
 
 #include <array>
 #include <atomic>
-#include <functional>
 #include <memory>
 #include <mutex>
 
 // No perfetto headers (other than tracing/api and protozero) should be here.
-#include "perfetto/tracing/buffer_exhausted_policy.h"
 #include "perfetto/tracing/core/data_source_config.h"
 #include "perfetto/tracing/internal/basic_types.h"
 #include "perfetto/tracing/trace_writer_base.h"
diff --git a/include/perfetto/tracing/internal/tracing_muxer.h b/include/perfetto/tracing/internal/tracing_muxer.h
index e7f4956..eb61a07 100644
--- a/include/perfetto/tracing/internal/tracing_muxer.h
+++ b/include/perfetto/tracing/internal/tracing_muxer.h
@@ -21,11 +21,13 @@
 #include <memory>
 
 #include "perfetto/base/export.h"
+#include "perfetto/tracing/buffer_exhausted_policy.h"
 #include "perfetto/tracing/core/forward_decls.h"
 #include "perfetto/tracing/interceptor.h"
 #include "perfetto/tracing/internal/basic_types.h"
 #include "perfetto/tracing/internal/tracing_tls.h"
 #include "perfetto/tracing/platform.h"
+
 namespace perfetto {
 
 class DataSourceBase;
diff --git a/include/perfetto/tracing/internal/track_event_internal.h b/include/perfetto/tracing/internal/track_event_internal.h
index 446c9e8..2afb9c0 100644
--- a/include/perfetto/tracing/internal/track_event_internal.h
+++ b/include/perfetto/tracing/internal/track_event_internal.h
@@ -271,8 +271,15 @@
       auto it_and_inserted = incr_state->seen_tracks.insert(uuid);
       if (PERFETTO_LIKELY(!it_and_inserted.second))
         return;
-      uuid = WriteTrackDescriptor(Track(uuid, Track()), trace_writer,
-                                  incr_state, tls_state, timestamp);
+      std::optional<TrackRegistry::TrackInfo> track_info =
+          TrackRegistry::Get()->FindTrackInfo(uuid);
+      if (!track_info) {
+        return;
+      }
+      TrackRegistry::WriteTrackDescriptor(
+          std::move(track_info->desc),
+          NewTracePacket(trace_writer, incr_state, tls_state, timestamp));
+      uuid = track_info->parent_uuid;
     }
   }
 
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 946d7d0..2a85729 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -39,6 +39,7 @@
 #include <stdint.h>
 #include <map>
 #include <mutex>
+#include <optional>
 
 namespace perfetto {
 namespace internal {
@@ -438,6 +439,10 @@
 class PERFETTO_EXPORT_COMPONENT TrackRegistry {
  public:
   using SerializedTrackDescriptor = std::string;
+  struct TrackInfo {
+    SerializedTrackDescriptor desc;
+    uint64_t parent_uuid = 0;
+  };
 
   TrackRegistry();
   ~TrackRegistry();
@@ -464,26 +469,31 @@
     // If the track has extra metadata (recorded with UpdateTrack), it will be
     // found in the registry. To minimize the time the lock is held, make a copy
     // of the data held in the registry and write it outside the lock.
-    std::string desc_copy;
-    uint64_t parent_uuid = 0;
-    {
-      std::lock_guard<std::mutex> lock(mutex_);
-      const auto& it = tracks_.find(track.uuid);
-      if (it != tracks_.end()) {
-        desc_copy = it->second.desc;
-        parent_uuid = it->second.parent_uuid;
-        PERFETTO_DCHECK(!desc_copy.empty());
-      }
-    }
-    if (!desc_copy.empty()) {
-      WriteTrackDescriptor(std::move(desc_copy), std::move(packet));
+    auto track_info = FindTrackInfo(track.uuid);
+    if (track_info) {
+      WriteTrackDescriptor(std::move(track_info->desc), std::move(packet));
+      return track_info->parent_uuid;
     } else {
       // Otherwise we just write the basic descriptor for this type of track
       // (e.g., just uuid, no name).
       track.Serialize(packet->set_track_descriptor());
-      parent_uuid = track.parent_uuid;
+      return track.parent_uuid;
     }
-    return parent_uuid;
+  }
+
+  // If saved in the registry, returns the serialize track descriptor and parent
+  // uuid for `uuid`.
+  std::optional<TrackInfo> FindTrackInfo(uint64_t uuid) {
+    std::optional<TrackInfo> track_info;
+    {
+      std::lock_guard<std::mutex> lock(mutex_);
+      const auto it = tracks_.find(uuid);
+      if (it != tracks_.end()) {
+        track_info = it->second;
+        PERFETTO_DCHECK(!track_info->desc.empty());
+      }
+    }
+    return track_info;
   }
 
   static void WriteTrackDescriptor(
@@ -491,10 +501,6 @@
       protozero::MessageHandle<protos::pbzero::TracePacket> packet);
 
  private:
-  struct TrackInfo {
-    SerializedTrackDescriptor desc;
-    uint64_t parent_uuid = 0;
-  };
   std::mutex mutex_;
   std::map<uint64_t /* uuid */, TrackInfo> tracks_;
 
diff --git a/infra/ci/config.py b/infra/ci/config.py
index 8994865..3546a9c 100755
--- a/infra/ci/config.py
+++ b/infra/ci/config.py
@@ -116,7 +116,7 @@
     'linux-clang-x86_64-bazel': {
         'PERFETTO_TEST_GN_ARGS': '',
         'PERFETTO_TEST_SCRIPT': 'test/ci/bazel_tests.sh',
-        'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '',
+        'PERFETTO_INSTALL_BUILD_DEPS_ARGS': '--bazel',
     },
     'ui-clang-x86_64-release': {
         'PERFETTO_TEST_GN_ARGS': 'is_debug=false',
diff --git a/infra/ci/sandbox/Dockerfile b/infra/ci/sandbox/Dockerfile
index 8968b31..90e9b9d 100644
--- a/infra/ci/sandbox/Dockerfile
+++ b/infra/ci/sandbox/Dockerfile
@@ -26,26 +26,21 @@
     apt-get -y install python3 python3-pip git curl sudo lz4 tar ccache tini \
                        libpulse0 libgl1 libxml2 libc6-dev-i386 libtinfo5 \
                        gnupg2 pkg-config zip g++ zlib1g-dev unzip \
-                       python3-distutils gcc-8 g++-8; \
+                       python3-distutils gcc-8 g++-8 \
+                       openjdk-11-jdk; \
     apt-get -y install libc++-8-dev libc++abi-8-dev clang-8; \
     update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1; \
     gcc-8 --version; \
     g++-8 --version; \
     clang-8 --version; \
     clang++-8 --version; \
+    java --version; \
     pip3 install protobuf pandas grpcio; \
     groupadd -g 1337 perfetto; \
     useradd -d /ci/ramdisk -u 1337 -g perfetto perfetto; \
     apt-get -y autoremove; \
     rm -rf /var/lib/apt/lists/* /usr/share/man/* /usr/share/doc/*;
 
-RUN set -ex; \
-    curl -LO https://github.com/bazelbuild/bazel/releases/download/7.0.2/bazel-7.0.2-installer-linux-x86_64.sh; \
-    chmod +x bazel-*-installer-linux-x86_64.sh; \
-    ./bazel-*-installer-linux-x86_64.sh; \
-    rm bazel-*-installer-linux-x86_64.sh; \
-    bazel version;
-
 # Chrome/puppeteer deps.
 RUN set -ex; \
     export DEBIAN_FRONTEND=noninteractive; \
diff --git a/perfetto.rc b/perfetto.rc
index 5c6d853..8169d8f 100644
--- a/perfetto.rc
+++ b/perfetto.rc
@@ -34,6 +34,7 @@
     onrestart exec_background - nobody shell -- /system/bin/traced_probes --cleanup-after-crash
     file /dev/kmsg w
     capabilities DAC_READ_SEARCH
+    shared_kallsyms
 
 on property:persist.device_config.global_settings.sys_traced=1
     setprop persist.traced.enable 1
diff --git a/protos/perfetto/config/chrome/scenario_config.proto b/protos/perfetto/config/chrome/scenario_config.proto
index eca7165..26bae25 100644
--- a/protos/perfetto/config/chrome/scenario_config.proto
+++ b/protos/perfetto/config/chrome/scenario_config.proto
@@ -116,6 +116,10 @@
   optional TraceConfig trace_config = 6;
 
   repeated NestedScenarioConfig nested_scenarios = 7;
+
+  // When set to true, this scenario initiates a tracing session using the
+  // system backend instead of the default in-browser custom backend.
+  optional bool use_system_backend = 8;
 }
 
 message ChromeFieldTracingConfig {
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index cddfe68..3b08662 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -20,7 +20,9 @@
     "ad_services_metric.proto",
     "android_anomaly_metric.proto",
     "android_blocking_call.proto",
+    "android_blocking_call_per_frame.proto",
     "android_blocking_calls_cuj_metric.proto",
+    "android_blocking_calls_cuj_per_frame_metric.proto",
     "android_blocking_calls_unagg.proto",
     "android_boot.proto",
     "android_boot_unagg.proto",
diff --git a/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto b/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
new file mode 100644
index 0000000..cf7d59c
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+// Blocking call per frame on the main thread.
+message AndroidBlockingCallPerFrame {
+    // Name of the blocking call
+    optional string name = 1;
+    // Maximal duration within a frame.
+    optional int64 max_dur_per_frame_ms = 2;
+    // Maximal duration within a frame in nanoseconds
+    optional int64 max_dur_per_frame_ns = 3;
+    // Mean duration within the CUJ
+    optional int64 mean_dur_per_frame_ms = 4;
+    // Mean duration within the CUJ in nanoseconds
+    optional int64 mean_dur_per_frame_ns = 5;
+    // Max count in a frame
+    optional int64 max_cnt_per_frame = 6;
+    // Mean count in a frame
+    optional double mean_cnt_per_frame = 7;
+}
diff --git a/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto b/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
new file mode 100644
index 0000000..abcca47
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/android_blocking_call_per_frame.proto";
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// Blocking calls per frame inside Android jank CUJs. Shows count and duration for each.
+message AndroidCujBlockingCallsPerFrameMetric {
+  repeated Cuj cuj = 1;
+
+  message Cuj {
+
+    // Name of the CUJ, extracted from the CUJ jank trace marker.
+    // For example SHADE_EXPAND_COLLAPSE from J<SHADE_EXPAND_COLLAPSE>.
+    optional string name = 1;
+
+    optional AndroidProcessMetadata process = 2;
+
+    // List of blocking calls on the process UI thread.
+    // Aggregation is done by CUJ name.
+    repeated AndroidBlockingCallPerFrame blocking_calls = 3;
+  }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index bbbca99..4c904ea 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -31,6 +31,7 @@
 import "protos/perfetto/metrics/android/batt_metric.proto";
 import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
 import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
+import "protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto";
 import "protos/perfetto/metrics/android/android_blocking_calls_unagg.proto";
 import "protos/perfetto/metrics/android/codec_metrics.proto";
 import "protos/perfetto/metrics/android/cpu_metric.proto";
@@ -80,7 +81,7 @@
 import "protos/perfetto/metrics/common/clone_duration.proto";
 
 // Trace processor metadata
-// Next id: 17
+// Next id: 18
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
@@ -90,6 +91,7 @@
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
+  optional string trace_causal_trigger = 17;
   optional string unique_session_name = 8;
   optional string trace_config_pbtxt = 9;
   optional int64 sched_duration_ns = 10;
@@ -353,6 +355,9 @@
 
   optional CloneDuration clone_duration = 77;
 
+  // Per-frame blocking calls (e.g. binder calls) during CUJs (important UI transitions).
+  optional AndroidCujBlockingCallsPerFrameMetric android_blocking_calls_cuj_per_frame_metric = 78;
+
   // 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 e73404b..78ebb81 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -176,6 +176,50 @@
 
 // End of protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
+
+// Blocking call per frame on the main thread.
+message AndroidBlockingCallPerFrame {
+    // Name of the blocking call
+    optional string name = 1;
+    // Maximal duration within a frame.
+    optional int64 max_dur_per_frame_ms = 2;
+    // Maximal duration within a frame in nanoseconds
+    optional int64 max_dur_per_frame_ns = 3;
+    // Mean duration within the CUJ
+    optional int64 mean_dur_per_frame_ms = 4;
+    // Mean duration within the CUJ in nanoseconds
+    optional int64 mean_dur_per_frame_ns = 5;
+    // Max count in a frame
+    optional int64 max_cnt_per_frame = 6;
+    // Mean count in a frame
+    optional double mean_cnt_per_frame = 7;
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_call_per_frame.proto
+
+// Begin of protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
+
+// Blocking calls per frame inside Android jank CUJs. Shows count and duration for each.
+message AndroidCujBlockingCallsPerFrameMetric {
+  repeated Cuj cuj = 1;
+
+  message Cuj {
+
+    // Name of the CUJ, extracted from the CUJ jank trace marker.
+    // For example SHADE_EXPAND_COLLAPSE from J<SHADE_EXPAND_COLLAPSE>.
+    optional string name = 1;
+
+    optional AndroidProcessMetadata process = 2;
+
+    // List of blocking calls on the process UI thread.
+    // Aggregation is done by CUJ name.
+    repeated AndroidBlockingCallPerFrame blocking_calls = 3;
+  }
+}
+
+// End of protos/perfetto/metrics/android/android_blocking_calls_cuj_per_frame_metric.proto
+
 // Begin of protos/perfetto/metrics/android/android_blocking_calls_unagg.proto
 
 // All blocking calls for a trace. Shows count and total duration for each.
@@ -3091,7 +3135,7 @@
 // Begin of protos/perfetto/metrics/metrics.proto
 
 // Trace processor metadata
-// Next id: 17
+// Next id: 18
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
@@ -3101,6 +3145,7 @@
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
+  optional string trace_causal_trigger = 17;
   optional string unique_session_name = 8;
   optional string trace_config_pbtxt = 9;
   optional int64 sched_duration_ns = 10;
@@ -3364,6 +3409,9 @@
 
   optional CloneDuration clone_duration = 77;
 
+  // Per-frame blocking calls (e.g. binder calls) during CUJs (important UI transitions).
+  optional AndroidCujBlockingCallsPerFrameMetric android_blocking_calls_cuj_per_frame_metric = 78;
+
   // Android
   // Demo extensions.
   extensions 450 to 499;
diff --git a/protos/perfetto/trace/android/BUILD.gn b/protos/perfetto/trace/android/BUILD.gn
index 429bbeb..320ce08 100644
--- a/protos/perfetto/trace/android/BUILD.gn
+++ b/protos/perfetto/trace/android/BUILD.gn
@@ -24,6 +24,7 @@
     "android_game_intervention_list.proto",
     "android_log.proto",
     "android_system_property.proto",
+    "bluetooth_trace.proto",
     "camera_event.proto",
     "frame_timeline_event.proto",
     "gpu_mem_event.proto",
diff --git a/protos/perfetto/trace/android/bluetooth_trace.proto b/protos/perfetto/trace/android/bluetooth_trace.proto
new file mode 100644
index 0000000..cbe8315
--- /dev/null
+++ b/protos/perfetto/trace/android/bluetooth_trace.proto
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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;
+
+// Describes the packet type and direction. CMD and EVT are unidirectional, so
+// no need to differentiate the direction.
+enum BluetoothTracePacketType {
+  HCI_CMD = 1;
+  HCI_EVT = 2;
+  HCI_ACL_RX = 3;
+  HCI_ACL_TX = 4;
+  HCI_SCO_RX = 5;
+  HCI_SCO_TX = 6;
+  HCI_ISO_RX = 7;
+  HCI_ISO_TX = 8;
+}
+
+// Trace event for bluetooth
+message BluetoothTraceEvent {
+
+  // Packet type and direction
+  optional BluetoothTracePacketType packet_type = 1;
+
+  // Total count of the packets collected during the collection interval
+  optional uint32 count = 2;
+
+  // Total cumulative length of the packets collected during the collection
+  // interval
+  optional uint32 length = 3;
+
+  // The collection interval in ms. This is the duration between the first and
+  // last packets collected.
+  optional uint32 duration = 4;
+
+  // In case of CMD type, further breakdown of the type of command
+  optional uint32 op_code = 5;
+
+  // In the case of EVT type, further breakdown of the type of event
+  optional uint32 event_code = 6;
+
+  // When applicable for EVT type, further breakdown of event type into specific
+  // subevent
+  optional uint32 subevent_code = 7;
+
+  // Associated handle for the bluetooth packet
+  optional uint32 connection_handle = 8;
+}
\ No newline at end of file
diff --git a/protos/perfetto/trace/chrome/v8.proto b/protos/perfetto/trace/chrome/v8.proto
index fdd551e..3a4c5c1 100644
--- a/protos/perfetto/trace/chrome/v8.proto
+++ b/protos/perfetto/trace/chrome/v8.proto
@@ -144,6 +144,8 @@
   // Where in the script source this function is defined. This is counted in
   // bytes not characters.
   optional uint32 byte_offset = 6;
+  optional uint32 line = 7;
+  optional uint32 column = 8;
 }
 
 // A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8
@@ -268,4 +270,4 @@
 
 message V8CodeDefaults {
   optional uint32 tid = 1;
-}
\ No newline at end of file
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index e3b8268..4969574 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -4810,6 +4810,53 @@
 
 // End of protos/perfetto/trace/android/android_system_property.proto
 
+// Begin of protos/perfetto/trace/android/bluetooth_trace.proto
+
+// Describes the packet type and direction. CMD and EVT are unidirectional, so
+// no need to differentiate the direction.
+enum BluetoothTracePacketType {
+  HCI_CMD = 1;
+  HCI_EVT = 2;
+  HCI_ACL_RX = 3;
+  HCI_ACL_TX = 4;
+  HCI_SCO_RX = 5;
+  HCI_SCO_TX = 6;
+  HCI_ISO_RX = 7;
+  HCI_ISO_TX = 8;
+}
+
+// Trace event for bluetooth
+message BluetoothTraceEvent {
+
+  // Packet type and direction
+  optional BluetoothTracePacketType packet_type = 1;
+
+  // Total count of the packets collected during the collection interval
+  optional uint32 count = 2;
+
+  // Total cumulative length of the packets collected during the collection
+  // interval
+  optional uint32 length = 3;
+
+  // The collection interval in ms. This is the duration between the first and
+  // last packets collected.
+  optional uint32 duration = 4;
+
+  // In case of CMD type, further breakdown of the type of command
+  optional uint32 op_code = 5;
+
+  // In the case of EVT type, further breakdown of the type of event
+  optional uint32 event_code = 6;
+
+  // When applicable for EVT type, further breakdown of event type into specific
+  // subevent
+  optional uint32 subevent_code = 7;
+
+  // Associated handle for the bluetooth packet
+  optional uint32 connection_handle = 8;
+}
+// End of protos/perfetto/trace/android/bluetooth_trace.proto
+
 // Begin of protos/perfetto/trace/android/camera_event.proto
 
 // A profiling event corresponding to a single camera frame. This message
@@ -6491,6 +6538,8 @@
   // Where in the script source this function is defined. This is counted in
   // bytes not characters.
   optional uint32 byte_offset = 6;
+  optional uint32 line = 7;
+  optional uint32 column = 8;
 }
 
 // A V8 Isolate instance. A V8 Isolate represents an isolated instance of the V8
@@ -6616,6 +6665,7 @@
 message V8CodeDefaults {
   optional uint32 tid = 1;
 }
+
 // End of protos/perfetto/trace/chrome/v8.proto
 
 // Begin of protos/perfetto/trace/clock_snapshot.proto
@@ -15688,7 +15738,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 114.
+// Next id: 115.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -15830,6 +15880,8 @@
 
     Trigger clone_snapshot_trigger = 113;
 
+    BluetoothTraceEvent bluetooth_trace_event = 114;
+
     // 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 6e84dc9..cbb8724 100644
--- a/protos/perfetto/trace/trace_packet.proto
+++ b/protos/perfetto/trace/trace_packet.proto
@@ -22,6 +22,7 @@
 import "protos/perfetto/trace/android/android_game_intervention_list.proto";
 import "protos/perfetto/trace/android/android_log.proto";
 import "protos/perfetto/trace/android/android_system_property.proto";
+import "protos/perfetto/trace/android/bluetooth_trace.proto";
 import "protos/perfetto/trace/android/camera_event.proto";
 import "protos/perfetto/trace/android/frame_timeline_event.proto";
 import "protos/perfetto/trace/android/gpu_mem_event.proto";
@@ -104,7 +105,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 114.
+// Next id: 115.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -246,6 +247,8 @@
 
     Trigger clone_snapshot_trigger = 113;
 
+    BluetoothTraceEvent bluetooth_trace_event = 114;
+
     // 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/python/generators/sql_processing/docs_parse.py b/python/generators/sql_processing/docs_parse.py
index 5b75b55..90c227b 100644
--- a/python/generators/sql_processing/docs_parse.py
+++ b/python/generators/sql_processing/docs_parse.py
@@ -74,7 +74,6 @@
   type: str
   long_type: str
   description: str
-  joinid_column: Optional[str]
 
 
 class AbstractDocParser(ABC):
@@ -166,9 +165,17 @@
 
       m = re.match(r'JOINID\(([_A-Za-z\.]*)\)', type)
       if m:
-        result[name] = Arg('JOINID', type, comment, m.groups()[0])
-      else:
-        result[name] = Arg(type, type, comment, None)
+        result[name] = Arg('JOINID', type, comment)
+        remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
+        continue
+
+      m = re.match(r'ID\(([_A-Za-z\.]*)\)', type)
+      if m:
+        result[name] = Arg('ID', type, comment)
+        remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
+        continue
+
+      result[name] = Arg(type, type, comment)
       # Strip whitespace and comma and parse the next arg.
       remaining_args = groups[-1].lstrip().lstrip(',').lstrip()
 
@@ -185,16 +192,12 @@
   type: str
   desc: str
   cols: Dict[str, Arg]
-  id_columns: List[str]
-  joinid_cols: Dict[str, Arg]
 
-  def __init__(self, name, type, desc, cols, id_columns, joinid_columns):
+  def __init__(self, name, type, desc, cols):
     self.name = name
     self.type = type
     self.desc = desc
     self.cols = cols
-    self.id_columns = id_columns
-    self.joinid_cols = joinid_columns
 
 
 class TableViewDocParser(AbstractDocParser):
@@ -236,22 +239,13 @@
       return
 
     cols = self._parse_columns(schema, ObjKind.table_view)
-    id_columns = []
-    joinid_cols = {}
 
-    for col_name, arg in cols.items():
-      if arg.type == "ID":
-        id_columns.append(col_name)
-      elif arg.type == "JOINID":
-        joinid_cols[col_name] = arg
 
     return TableOrView(
         name=self._parse_name(),
         type=type,
         desc=self._parse_desc_not_empty(doc.description),
-        cols=self._parse_columns(schema, ObjKind.table_view),
-        id_columns=id_columns,
-        joinid_columns=joinid_cols)
+        cols=self._parse_columns(schema, ObjKind.table_view))
 
 
 class Function:
@@ -443,7 +437,6 @@
   table_functions: List[TableFunction] = []
   macros: List[Macro] = []
   includes: List[Include]
-  id_columns: Dict[str, List[str]]
 
   def __init__(self, package_name: str, module_as_list: List[str],
                errors: List[str], table_views: List[TableOrView],
@@ -458,7 +451,6 @@
     self.table_functions = table_functions
     self.macros = macros
     self.includes = includes
-    self.id_columns = {o.name: o.id_columns for o in table_views}
 
 
 def parse_file(path: str, sql: str) -> Optional[ParsedModule]:
diff --git a/python/generators/trace_processor_table/serialize.py b/python/generators/trace_processor_table/serialize.py
index dedfe51..de6a94f 100644
--- a/python/generators/trace_processor_table/serialize.py
+++ b/python/generators/trace_processor_table/serialize.py
@@ -41,7 +41,6 @@
     self.data_layer_type = data_layer_type(table.table, self.parsed_col)
 
     self.is_implicit_id = self.parsed_col.is_implicit_id
-    self.is_implicit_type = self.parsed_col.is_implicit_type
     self.is_ancestor = self.parsed_col.is_ancestor
     self.is_string = parsed_type.cpp_type == 'StringPool::Id'
     self.is_optional = parsed_type.is_optional
@@ -53,26 +52,26 @@
     return f'    using {self.name} = {self.typed_column_type};'
 
   def row_field(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
     return f'    {self.cpp_type} {self.name};'
 
   def row_param(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     return f'{self.cpp_type} in_{self.name} = {{}}'
 
   def parent_row_initializer(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if not self.is_ancestor:
       return None
     return f'in_{self.name}'
 
   def row_initializer(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -84,7 +83,7 @@
     }}'''
 
   def row_ref_getter(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     return f'''void set_{self.name}(
         ColumnType::{self.name}::non_optional_type v) {{
@@ -92,7 +91,7 @@
     }}'''
 
   def flag(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -106,7 +105,7 @@
     '''
 
   def storage_init(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -116,7 +115,7 @@
     return f'''{self.name}_({storage}::Create<{dense}>())'''
 
   def column_init(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -133,7 +132,7 @@
     return f'    {self.name}_.ShrinkToFit();'
 
   def append(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -148,7 +147,7 @@
   '''
 
   def mutable_accessor(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     return f'''
   {self.typed_column_type}* mutable_{self.name}() {{
@@ -158,7 +157,7 @@
   '''
 
   def storage(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -176,7 +175,7 @@
     '''
 
   def static_schema(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     return f'''
     schema.columns.emplace_back(Table::Schema::Column{{
@@ -187,26 +186,26 @@
     '''
 
   def row_eq(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     return f'ColumnType::{self.name}::Equals({self.name}, other.{self.name})'
 
   def extend_parent_param(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
     return f'ColumnStorage<ColumnType::{self.name}::stored_type> {self.name}'
 
   def extend_parent_param_arg(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
     return f'std::move({self.name})'
 
   def static_assert_flags(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
@@ -218,7 +217,7 @@
     '''
 
   def extend_nullable_vector(self) -> Optional[str]:
-    if self.is_implicit_id or self.is_implicit_type:
+    if self.is_implicit_id:
       return None
     if self.is_ancestor:
       return None
diff --git a/python/generators/trace_processor_table/util.py b/python/generators/trace_processor_table/util.py
index 6896d94..e301e17 100644
--- a/python/generators/trace_processor_table/util.py
+++ b/python/generators/trace_processor_table/util.py
@@ -78,10 +78,6 @@
   # parsing the tables rather than by the user.
   is_implicit_id: bool = False
 
-  # Whether this column is the implicit "type" column which is added by while
-  # parsing the tables rather than by the user.
-  is_implicit_type: bool = False
-
   # Whether this column comes from copying a column from the ancestor. If this
   # is set to false, the user explicitly specified it for this table.
   is_ancestor: bool = False
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index 1bebad7..40e68df 100755
--- a/python/perfetto/prebuilts/manifests/trace_processor_shell.py
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9949656,
+        10524008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
+        '867c70800cfe81c2640f2aae8bb58eca68fa1389a3258a25c285ee5510edbbe3',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9223224,
+        9767976,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/trace_processor_shell',
     'sha256':
-        '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
+        '9c325030078bc4de8693083c9e4e2b72c83ca694c3a4ef8cc1bd9c29fb421815',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10142800,
+        10935344,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
+        '6af6f87e6521eec186e74c68c0c6eeeeb557556e368d0e4f563be5ce5d9d936b',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7329432,
+        8010576,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/trace_processor_shell',
     'sha256':
-        '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
+        'da0c361d4a2c8d8b2d1ffd45cd388d964cc58b09e8e41f48aa045ed357510755',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9703384,
+        10448648,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/trace_processor_shell',
     'sha256':
-        'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
+        'ecb6a1a073eb4bbfe36af56ab4406671e8febe02fb4c6dcef73fb1fe5d817fad',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7367412,
+        7905948,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/trace_processor_shell',
     'sha256':
-        'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
+        'efeed53819e11f5f82d1ec9c3edce00590b3db1e3e4f0e64acddafba2c35a52e'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9598784,
+        10154712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/trace_processor_shell',
     'sha256':
-        '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
+        'c6b0bb85228e4a3785030bbaf718bb428580f7d31de127e73042a30b9a67128a'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10625488,
+        11244904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/trace_processor_shell',
     'sha256':
-        '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
+        'a113d0f68b89c63da4faefebc094a5be15620afdbe862a23127d55d7ff44ed43'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9915664,
+        10506544,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/trace_processor_shell',
     'sha256':
-        'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
+        'f062e38fd28ab94b0232df8f4eeac70506bb7d304b82100e288d5acb603f84c1'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        9922560,
+        10479616,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
+        'a881f3e2d4c6131493e85bfd1f36d1efe58e1478e2991825418d5d21614c1e48',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index 0745b63..31a0afe 100755
--- a/python/perfetto/prebuilts/manifests/tracebox.py
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1613864,
+        1646808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
     'sha256':
-        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+        '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1492184,
+        1508856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
     'sha256':
-        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+        'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2380040,
+        2415168,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
     'sha256':
-        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+        '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1450708,
+        1478024,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
     'sha256':
-        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+        '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2269816,
+        2304384,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
     'sha256':
-        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+        '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,42 +75,42 @@
     'file_name':
         'tracebox',
     'file_size':
-        1333336,
+        1354916,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
     'sha256':
-        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+        '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2115984,
+        2142008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
     'sha256':
-        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+        '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2302960,
+        2341884,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
     'sha256':
-        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+        '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2147880,
+        2178416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
     'sha256':
-        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+        '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
 }]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index abe58a0..f492f09 100755
--- a/python/perfetto/prebuilts/manifests/traceconv.py
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9041560,
+        9599720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
     'sha256':
-        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+        '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8375512,
+        8920424,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
     'sha256':
-        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+        '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9134136,
+        9920848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
     'sha256':
-        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+        '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6753020,
+        7430084,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
     'sha256':
-        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+        '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8740064,
+        9479552,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
     'sha256':
-        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+        '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6792280,
+        7329320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
     'sha256':
-        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+        '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        8677992,
+        9232824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
     'sha256':
-        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+        'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9503704,
+        10121840,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
     'sha256':
-        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+        '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8964488,
+        9554408,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
     'sha256':
-        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+        'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8763904,
+        9316864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
     'sha256':
-        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+        '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 6041254..eb3f9b0 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/test/stdlib_unittest.py b/python/test/stdlib_unittest.py
index 6f4c98d..f0f3023 100644
--- a/python/test/stdlib_unittest.py
+++ b/python/test/stdlib_unittest.py
@@ -42,7 +42,7 @@
     self.assertEqual(fn.name, 'cr_table')
     self.assertEqual(fn.desc, 'Comment')
     self.assertEqual(fn.cols, {
-        'x': Arg('LONG', 'LONG', 'Column.', None),
+        'x': Arg('LONG', 'LONG', 'Column.'),
     })
 
   # Checks that when custom prefixes (cr for chrome/util) are present,
@@ -63,7 +63,7 @@
     self.assertEqual(fn.name, 'chrome_table')
     self.assertEqual(fn.desc, 'Comment')
     self.assertEqual(fn.cols, {
-        'x': Arg('LONG', 'LONG', 'Column.', None),
+        'x': Arg('LONG', 'LONG', 'Column.'),
     })
 
   # Checks that when custom prefixes (cr for chrome/util) are present,
@@ -166,7 +166,7 @@
     self.assertEqual(table.desc, 'Table comment.')
     self.assertEqual(table.type, 'TABLE')
     self.assertEqual(table.cols, {
-        'id': Arg('LONG', 'LONG', 'Id of slice.', None),
+        'id': Arg('LONG', 'LONG', 'Id of slice.'),
     })
 
   def test_perfetto_view_with_schema(self):
@@ -189,8 +189,8 @@
     self.assertEqual(table.type, 'VIEW')
     self.assertEqual(
         table.cols, {
-            'foo': Arg('LONG', 'LONG', 'Foo.', None),
-            'bar': Arg('STRING', 'STRING', 'Bar.', None),
+            'foo': Arg('LONG', 'LONG', 'Foo.'),
+            'bar': Arg('STRING', 'STRING', 'Bar.'),
         })
 
   def test_function_with_new_style_docs(self):
@@ -214,8 +214,8 @@
     self.assertEqual(fn.desc, 'Function foo.')
     self.assertEqual(
         fn.args, {
-            'utid': Arg('LONG', 'LONG', 'Utid of thread.', None),
-            'name': Arg('STRING', 'STRING', 'String name.', None),
+            'utid': Arg('LONG', 'LONG', 'Utid of thread.'),
+            'name': Arg('STRING', 'STRING', 'String name.'),
         })
     self.assertEqual(fn.return_type, 'BOOL')
     self.assertEqual(fn.return_desc, 'Exists.')
@@ -241,10 +241,10 @@
     self.assertEqual(fn.name, 'foo_fn')
     self.assertEqual(fn.desc, 'Function foo.')
     self.assertEqual(fn.args, {
-        'utid': Arg('LONG', 'LONG', 'Utid of thread.', None),
+        'utid': Arg('LONG', 'LONG', 'Utid of thread.'),
     })
     self.assertEqual(fn.cols, {
-        'count': Arg('LONG', 'LONG', 'Count.', None),
+        'count': Arg('LONG', 'LONG', 'Count.'),
     })
 
   def test_function_with_new_style_docs_multiline_comment(self):
@@ -268,7 +268,7 @@
     self.assertEqual(fn.name, 'foo_fn')
     self.assertEqual(fn.desc, 'Function foo.')
     self.assertEqual(fn.args, {
-        'arg': Arg('LONG', 'LONG', 'Multi line  comment.', None),
+        'arg': Arg('LONG', 'LONG', 'Multi line  comment.'),
     })
     self.assertEqual(fn.return_type, 'BOOL')
     self.assertEqual(fn.return_desc, 'Exists.')
@@ -294,7 +294,7 @@
     self.assertEqual(fn.name, 'foo_fn')
     self.assertEqual(fn.desc, 'Function foo.')
     self.assertEqual(fn.args, {
-        'arg': Arg('LONG', 'LONG', 'Arg', None),
+        'arg': Arg('LONG', 'LONG', 'Arg'),
     })
     self.assertEqual(fn.return_type, 'BOOL')
     self.assertEqual(fn.return_desc, 'Multi line return comment.')
@@ -360,7 +360,7 @@
     self.assertEqual(fn.name, 'foo_fn')
     self.assertEqual(fn.desc, 'Function foo.')
     self.assertEqual(fn.args, {
-        'utid': Arg('LONG', 'LONG', 'Utid of thread (important).', None),
+        'utid': Arg('LONG', 'LONG', 'Utid of thread (important).'),
     })
     self.assertEqual(fn.return_type, 'BOOL')
     self.assertEqual(fn.return_desc, 'Exists.')
@@ -396,7 +396,7 @@
     self.assertEqual(macro.name, 'foo_macro')
     self.assertEqual(macro.desc, 'Macro')
     self.assertEqual(macro.args, {
-        'x': Arg('TableOrSubquery', 'TableOrSubquery', 'x Arg.', None),
+        'x': Arg('TableOrSubquery', 'TableOrSubquery', 'x Arg.'),
     })
     self.assertEqual(macro.return_type, 'TableOrSubquery')
     self.assertEqual(macro.return_desc, 'Exists.')
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 67a5990..1364094 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -91,7 +91,10 @@
         ['/core/*', '/frontend/*', '/common/actions'],
     ),
 
-    # Miscl legitimate deps.
+    # The record plugin needs access to the wasm .d.ts for trace_config_utils.
+    ('/plugins/dev.perfetto.RecordTrace*', '/gen/trace_config_utils'),
+
+    # Misc legitimate deps.
     ('/frontend/index', ['/gen/*']),
     ('/traceconv/index', '/gen/traceconv'),
     ('/engine/wasm_bridge', '/gen/trace_processor'),
@@ -195,7 +198,7 @@
     return
   if len(all_deps) > 1:
     raise Exception('Ambiguous plugin deps in %s: %s' % (path, all_deps))
-  declared_deps = re.sub('\s*', '', all_deps[0]).split(',')
+  declared_deps = [x for x in re.sub('\s*', '', all_deps[0]).split(',') if x]
   for imported_as in declared_deps:
     resolved_dep = import_map.get(imported_as)
     if resolved_dep is None:
diff --git a/src/kallsyms/kernel_symbol_map.cc b/src/kallsyms/kernel_symbol_map.cc
index 4ff73d1..be71fff 100644
--- a/src/kallsyms/kernel_symbol_map.cc
+++ b/src/kallsyms/kernel_symbol_map.cc
@@ -27,6 +27,7 @@
 #include "perfetto/protozero/proto_utils.h"
 
 #include <stdio.h>
+#include <unistd.h>
 
 #include <algorithm>
 #include <cinttypes>
@@ -51,17 +52,11 @@
 constexpr size_t kSymNameMaxLen = 128;
 constexpr size_t kSymMaxSizeBytes = 1024 * 1024;
 
-// Reads a kallsyms file in blocks of 4 pages each and decode its lines using
-// a simple FSM. Calls the passed lambda for each valid symbol.
-// It skips undefined symbols and other useless stuff.
+// Reads a kallsyms file and decodes its lines using a simple FSM. Calls the
+// passed lambda for each valid symbol. It skips undefined symbols and other
+// useless stuff.
 template <typename Lambda /* void(uint64_t, char, base::StringView) */>
-void ForEachSym(const std::string& kallsyms_path, Lambda fn) {
-  base::ScopedFile fd = base::OpenFile(kallsyms_path.c_str(), O_RDONLY);
-  if (!fd) {
-    PERFETTO_PLOG("Cannot open %s", kallsyms_path.c_str());
-    return;
-  }
-
+void ForEachSym(int fd, Lambda fn) {
   // /proc/kallsyms looks as follows:
   // 0000000000026a80 A bpf_trace_sds
   //
@@ -75,15 +70,20 @@
   static constexpr size_t kBufSize = 16 * 1024;
   base::PagedMemory buffer = base::PagedMemory::Allocate(kBufSize);
   enum { kSymAddr, kSymType, kSymName, kEatRestOfLine } state = kSymAddr;
+  off_t rd_offset = 0;
   uint64_t sym_addr = 0;
   char sym_type = '\0';
   char sym_name[kSymNameMaxLen + 1];
   size_t sym_name_len = 0;
   for (;;) {
     char* buf = static_cast<char*>(buffer.Get());
-    auto rsize = base::Read(*fd, buf, kBufSize);
+    // Use pread because on android we might be sharing an open file across
+    // processes. Even if they should be mutually excluded, not relying on a
+    // seek position is simpler to reason about.
+    ssize_t rsize = PERFETTO_EINTR(pread(fd, buf, kBufSize, rd_offset));
+    rd_offset += rsize;
     if (rsize < 0) {
-      PERFETTO_PLOG("read(%s) failed", kallsyms_path.c_str());
+      PERFETTO_PLOG("pread(kallsyms) failed");
       return;
     }
     if (rsize == 0)
@@ -234,7 +234,7 @@
   return base::StringView();
 }
 
-size_t KernelSymbolMap::Parse(const std::string& kallsyms_path) {
+size_t KernelSymbolMap::Parse(int fd) {
   PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, KALLSYMS_PARSE);
   using SymAddr = uint64_t;
 
@@ -263,8 +263,12 @@
   // Based on `cat /proc/kallsyms | egrep "\b[tT]\b" | wc -l`.
   symbol_tokens.reserve(128 * 1024);
 
-  ForEachSym(kallsyms_path, [&](SymAddr addr, char type,
-                                base::StringView name) {
+  if (fd < 0) {
+    PERFETTO_ELOG("Invalid kallsyms fd");
+    return 0;
+  }
+
+  ForEachSym(fd, [&](SymAddr addr, char type, base::StringView name) {
     // Special cases:
     //
     // Skip arm mapping symbols such as $x, $x.123, $d, $d.123. They exist to
diff --git a/src/kallsyms/kernel_symbol_map.h b/src/kallsyms/kernel_symbol_map.h
index c69d86a..bf26eb4 100644
--- a/src/kallsyms/kernel_symbol_map.h
+++ b/src/kallsyms/kernel_symbol_map.h
@@ -24,6 +24,8 @@
 #include <string>
 #include <vector>
 
+#include "perfetto/ext/base/scoped_file.h"
+
 namespace perfetto {
 
 namespace base {
@@ -123,7 +125,8 @@
   static size_t kTokenIndexSampling;
 
   // Parses a kallsyms file. Returns the number of valid symbols decoded.
-  size_t Parse(const std::string& kallsyms_path);
+  // Does not take ownership of the fd.
+  size_t Parse(int fd);
 
   // Looks up the closest symbol (i.e. the one with the highest address <=
   // |addr|) from its absolute 64-bit address.
diff --git a/src/kallsyms/kernel_symbol_map_benchmark.cc b/src/kallsyms/kernel_symbol_map_benchmark.cc
index 70bd8d4..08ddf30 100644
--- a/src/kallsyms/kernel_symbol_map_benchmark.cc
+++ b/src/kallsyms/kernel_symbol_map_benchmark.cc
@@ -101,7 +101,9 @@
   // which slows down significantly the CI.
   const bool skip = IsBenchmarkFunctionalOnly();
   if (!skip) {
-    kallsyms.Parse(perfetto::base::GetTestDataPath("test/data/kallsyms.txt"));
+    auto fd = perfetto::base::OpenFile(
+        perfetto::base::GetTestDataPath("test/data/kallsyms.txt"), O_RDONLY);
+    kallsyms.Parse(*fd);
   }
 
   for (auto _ : state) {
@@ -137,7 +139,8 @@
   for (auto _ : state) {
     perfetto::KernelSymbolMap kallsyms;
     if (!skip) {
-      kallsyms.Parse(kallsyms_path);
+      auto fd = perfetto::base::OpenFile(kallsyms_path, O_RDONLY);
+      kallsyms.Parse(*fd);
     }
   }
 }
diff --git a/src/kallsyms/kernel_symbol_map_unittest.cc b/src/kallsyms/kernel_symbol_map_unittest.cc
index 7a00bc0..bab6c70 100644
--- a/src/kallsyms/kernel_symbol_map_unittest.cc
+++ b/src/kallsyms/kernel_symbol_map_unittest.cc
@@ -100,7 +100,7 @@
   KernelSymbolMap kallsyms;
   EXPECT_EQ(kallsyms.Lookup(0x42), "");
 
-  kallsyms.Parse(tmp.path().c_str());
+  kallsyms.Parse(*base::OpenFile(tmp.path().c_str(), O_RDONLY));
   EXPECT_EQ(kallsyms.num_syms(), 10u);
 
   // Test first exact lookups.
@@ -157,7 +157,7 @@
   base::FlushFile(tmp.fd());
 
   KernelSymbolMap kallsyms;
-  kallsyms.Parse(tmp.path().c_str());
+  kallsyms.Parse(*base::OpenFile(tmp.path().c_str(), O_RDONLY));
   ASSERT_EQ(kallsyms.num_syms(), symbols.size());
   for (const auto& kv : symbols) {
     ASSERT_EQ(kallsyms.Lookup(kv.first), kv.second);
diff --git a/src/kallsyms/lazy_kernel_symbolizer.cc b/src/kallsyms/lazy_kernel_symbolizer.cc
index 8ed0af8..474ab45 100644
--- a/src/kallsyms/lazy_kernel_symbolizer.cc
+++ b/src/kallsyms/lazy_kernel_symbolizer.cc
@@ -18,49 +18,78 @@
 
 #include <string>
 
+#include <sys/file.h>
 #include <unistd.h>
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/compiler.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 #include "src/kallsyms/kernel_symbol_map.h"
 
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#include <sys/system_properties.h>
-#endif
-
 namespace perfetto {
-
 namespace {
 
 const char kKallsymsPath[] = "/proc/kallsyms";
 const char kPtrRestrictPath[] = "/proc/sys/kernel/kptr_restrict";
-const char kLowerPtrRestrictAndroidProp[] = "security.lower_kptr_restrict";
+const char kEnvName[] = "ANDROID_FILE__proc_kallsyms";
 
-// This class takes care of temporarily lowering kptr_restrict and putting it
-// back to the original value if necessary. It solves the following problem:
-// When reading /proc/kallsyms on Linux/Android, the symbol addresses can be
-// masked out (i.e. they are all 00000000) through the kptr_restrict file.
-// On Android kptr_restrict defaults to 2. On Linux, it depends on the
-// distribution. On Android we cannot simply write() kptr_restrict ourselves.
-// Doing so requires the union of:
-// - filesystem ACLs: kptr_restrict is rw-r--r--// and owned by root.
-// - Selinux rules: kptr_restrict is labelled as proc_security and restricted.
-// - CAP_SYS_ADMIN: when writing to kptr_restrict, the kernel enforces that the
-//                  caller has the SYS_ADMIN capability at write() time.
-// The latter would be problematic, we don't want traced_probes to have that,
-// CAP_SYS_ADMIN is too broad.
-// Instead, we opt for the following model: traced_probes sets an Android
-// property introduced in S (security.lower_kptr_restrict); init (which
-// satisfies all the requirements above) in turn sets kptr_restrict.
-// On Linux and standalone builds, instead, we don't have many options. Either:
-// - The system administrator takes care of lowering kptr_restrict before
-//   tracing.
-// - The system administrator runs traced_probes as root / CAP_SYS_ADMIN and we
-//   temporarily lower and restore kptr_restrict ourselves.
-// This class deals with all these cases.
+size_t ParseInheritedAndroidKallsyms(KernelSymbolMap* symbol_map) {
+  const char* fd_str = getenv(kEnvName);
+  auto inherited_fd = base::CStringToInt32(fd_str ? fd_str : "");
+  // Note: this is also the early exit for non-platform builds.
+  if (!inherited_fd.has_value()) {
+    PERFETTO_DLOG("Failed to parse %s (%s)", kEnvName, fd_str ? fd_str : "N/A");
+    return 0;
+  }
+
+  // We've inherited a special fd for kallsyms from init, but we might be
+  // sharing the underlying open file description with a concurrent process.
+  // Even if we use pread() for reading at absolute offsets, the underlying
+  // kernel seqfile is stateful and remembers where the last read stopped. In
+  // the worst case, two concurrent readers will cause a quadratic slowdown
+  // since the kernel reconstructs the seqfile from the beginning whenever two
+  // reads are not consequent.
+  // The chosen approach is to use provisional file locks to coordinate access.
+  // However we cannot use the special fd for locking, since the locks are based
+  // on the underlying open file description (in other words, both sharers will
+  // think they own the same lock). Therefore we open /proc/kallsyms again
+  // purely for locking purposes.
+  base::ScopedFile fd_for_lock = base::OpenFile(kKallsymsPath, O_RDONLY);
+  if (!fd_for_lock) {
+    PERFETTO_PLOG("Failed to open kallsyms for locking.");
+    return 0;
+  }
+
+  // Blocking lock since the only possible contention is
+  // traced_probes<->traced_perf, which will both lock only for the duration of
+  // the parse. Worst case, the task watchdog will restart the process.
+  //
+  // Lock goes away when |fd_for_lock| gets closed at end of scope.
+  if (flock(*fd_for_lock, LOCK_EX) != 0) {
+    PERFETTO_PLOG("Unexpected error in flock(kallsyms).");
+    return 0;
+  }
+
+  return symbol_map->Parse(*inherited_fd);
+}
+
+// This class takes care of temporarily lowering the kptr_restrict sysctl.
+// Otherwise the symbol addresses in /proc/kallsyms will be zeroed out on most
+// Linux configurations.
+//
+// On Android platform builds, this is solved by inheriting a kallsyms fd from
+// init, with symbols being visible as that is evaluated at the time of the
+// initial open().
+//
+// On Linux and standalone builds, we rely on this class in combination with
+// either:
+// - the sysctls (kptr_restrict, perf_event_paranoid) or this process'
+//   capabilitied to be sufficient for addresses to be visible.
+// - this process to be running as root / CAP_SYS_ADMIN, in which case this
+//   class will attempt to temporarily override kptr_restrict ourselves.
 class ScopedKptrUnrestrict {
  public:
   ScopedKptrUnrestrict();   // Lowers kptr_restrict if necessary.
@@ -69,46 +98,15 @@
  private:
   static void WriteKptrRestrict(const std::string&);
 
-  static const bool kUseAndroidProperty;
   std::string initial_value_;
-  bool restore_on_dtor_ = true;
 };
 
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-// This is true only on Android in-tree builds (not on standalone).
-const bool ScopedKptrUnrestrict::kUseAndroidProperty = true;
-#else
-const bool ScopedKptrUnrestrict::kUseAndroidProperty = false;
-#endif
-
 ScopedKptrUnrestrict::ScopedKptrUnrestrict() {
   if (LazyKernelSymbolizer::CanReadKernelSymbolAddresses()) {
-    // Everything seems to work (e.g., we are running as root and kptr_restrict
-    // is < 2). Don't touching anything.
-    restore_on_dtor_ = false;
+    // Symbols already visible, don't touch anything.
     return;
   }
 
-  if (kUseAndroidProperty) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-    __system_property_set(kLowerPtrRestrictAndroidProp, "1");
-#endif
-    // Init takes some time to react to the property change.
-    // Unfortunately, we cannot read kptr_restrict because of SELinux. Instead,
-    // we detect this by reading the initial lines of kallsyms and checking
-    // that they are non-zero. This loop waits for at most 250ms (50 * 5ms).
-    for (int attempt = 1; attempt <= 50; ++attempt) {
-      usleep(5000);
-      if (LazyKernelSymbolizer::CanReadKernelSymbolAddresses())
-        return;
-    }
-    PERFETTO_ELOG("kallsyms addresses are still masked after setting %s",
-                  kLowerPtrRestrictAndroidProp);
-    return;
-  }  // if (kUseAndroidProperty)
-
-  // On Linux and Android standalone, read the kptr_restrict value and lower it
-  // if needed.
   bool read_res = base::ReadFile(kPtrRestrictPath, &initial_value_);
   if (!read_res) {
     PERFETTO_PLOG("Failed to read %s", kPtrRestrictPath);
@@ -124,15 +122,9 @@
 }
 
 ScopedKptrUnrestrict::~ScopedKptrUnrestrict() {
-  if (!restore_on_dtor_)
+  if (initial_value_.empty())
     return;
-  if (kUseAndroidProperty) {
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-    __system_property_set(kLowerPtrRestrictAndroidProp, "0");
-#endif
-  } else if (!initial_value_.empty()) {
-    WriteKptrRestrict(initial_value_);
-  }
+  WriteKptrRestrict(initial_value_);
 }
 
 void ScopedKptrUnrestrict::WriteKptrRestrict(const std::string& value) {
@@ -140,8 +132,9 @@
   PERFETTO_DCHECK(!value.empty());
   base::ScopedFile fd = base::OpenFile(kPtrRestrictPath, O_WRONLY);
   auto wsize = write(*fd, value.c_str(), value.size());
-  if (wsize <= 0)
+  if (wsize <= 0) {
     PERFETTO_PLOG("Failed to set %s to %s", kPtrRestrictPath, value.c_str());
+  }
 }
 
 }  // namespace
@@ -154,12 +147,19 @@
   if (symbol_map_)
     return symbol_map_.get();
 
-  symbol_map_.reset(new KernelSymbolMap());
+  symbol_map_ = std::make_unique<KernelSymbolMap>();
 
-  // If kptr_restrict is set, try temporarily lifting it (it works only if
-  // traced_probes is run as a privileged user).
+  // Android platform builds: we have an fd from init.
+  size_t num_syms = ParseInheritedAndroidKallsyms(symbol_map_.get());
+  if (num_syms) {
+    return symbol_map_.get();
+  }
+
+  // Otherwise, try reading the file directly, temporarily lowering
+  // kptr_restrict if we're running with sufficient privileges.
   ScopedKptrUnrestrict kptr_unrestrict;
-  symbol_map_->Parse(kKallsymsPath);
+  auto fd = base::OpenFile(kKallsymsPath, O_RDONLY);
+  symbol_map_->Parse(*fd);
   return symbol_map_.get();
 }
 
diff --git a/src/profiling/common/producer_support.cc b/src/profiling/common/producer_support.cc
index 5303658..e9e193d 100644
--- a/src/profiling/common/producer_support.cc
+++ b/src/profiling/common/producer_support.cc
@@ -16,6 +16,7 @@
 
 #include "src/profiling/common/producer_support.h"
 
+#include <algorithm>
 #include <optional>
 
 #include "perfetto/ext/base/android_utils.h"
diff --git a/src/profiling/symbolizer/local_symbolizer.cc b/src/profiling/symbolizer/local_symbolizer.cc
index 5271e82..4121687 100644
--- a/src/profiling/symbolizer/local_symbolizer.cc
+++ b/src/profiling/symbolizer/local_symbolizer.cc
@@ -347,12 +347,8 @@
         return;
       }
       ssize_t rd = base::Read(*fd, &magic, sizeof(magic));
-      if (rd != sizeof(magic)) {
-        PERFETTO_PLOG("Failed to read %s", fname);
-        return;
-      }
-      if (!IsElf(magic, static_cast<size_t>(rd)) &&
-          !IsMachO64(magic, static_cast<size_t>(rd))) {
+      if (rd != sizeof(magic) || (!IsElf(magic, static_cast<size_t>(rd)) &&
+                                  !IsMachO64(magic, static_cast<size_t>(rd)))) {
         PERFETTO_DLOG("%s not an ELF or Mach-O 64.", fname);
         return;
       }
diff --git a/src/protozero/scattered_stream_writer.cc b/src/protozero/scattered_stream_writer.cc
index 2743a68..ce138cc 100644
--- a/src/protozero/scattered_stream_writer.cc
+++ b/src/protozero/scattered_stream_writer.cc
@@ -66,11 +66,13 @@
 // TODO(primiano): perf optimization: I suspect that at the end this will always
 // be called with |size| == 4, in which case we might just hardcode it.
 uint8_t* ScatteredStreamWriter::ReserveBytes(size_t size) {
-  if (write_ptr_ + size > cur_range_.end) {
+  PERFETTO_DCHECK(write_ptr_ <= cur_range_.end);
+  if (size > static_cast<size_t>(cur_range_.end - write_ptr_)) {
     // Assume the reservations are always < Delegate::GetNewBuffer().size(),
     // so that one single call to Extend() will definitely give enough headroom.
     Extend();
-    PERFETTO_DCHECK(write_ptr_ + size <= cur_range_.end);
+    PERFETTO_DCHECK(write_ptr_ <= cur_range_.end);
+    PERFETTO_DCHECK(size <= static_cast<size_t>(cur_range_.end - write_ptr_));
   }
   uint8_t* begin = write_ptr_;
   write_ptr_ += size;
diff --git a/src/shared_lib/BUILD.gn b/src/shared_lib/BUILD.gn
index 0e3115b..ee0aa49 100644
--- a/src/shared_lib/BUILD.gn
+++ b/src/shared_lib/BUILD.gn
@@ -34,7 +34,6 @@
     "../../include/perfetto/public",
     "../base",
     "../tracing:client_api",
-    "../tracing:platform_impl",
   ]
   sources = [
     "data_source.cc",
diff --git a/src/shared_lib/data_source.cc b/src/shared_lib/data_source.cc
index 32b57f8..64ad670 100644
--- a/src/shared_lib/data_source.cc
+++ b/src/shared_lib/data_source.cc
@@ -78,6 +78,25 @@
   }
 };
 
+struct PerfettoDsOnStopArgs {
+  struct PerfettoDsAsyncStopper* stopper = nullptr;
+};
+
+struct PerfettoDsAsyncStopper {
+  PerfettoDsImpl* ds_impl;
+  uint32_t instance_idx;
+  std::function<void()> async_stop_closure;
+
+  void FinishStop() {
+    std::lock_guard<std::mutex> lock(ds_impl->mu);
+    ds_impl->enabled_instances.reset(instance_idx);
+    if (ds_impl->enabled_instances.none()) {
+      ds_impl->enabled.store(false, std::memory_order_release);
+    }
+    async_stop_closure();
+  }
+};
+
 namespace perfetto {
 namespace shlib {
 
@@ -125,17 +144,24 @@
   }
 
   void OnStop(const StopArgs& args) override {
+    PerfettoDsOnStopArgs c_args;
+    c_args.stopper = new PerfettoDsAsyncStopper();
+    // Capturing ds_impl is ok, because data sources cannot be unregistered.
+    c_args.stopper->ds_impl = &type_;
+    c_args.stopper->async_stop_closure = args.HandleStopAsynchronously();
+    c_args.stopper->instance_idx = args.internal_instance_index;
+
     if (type_.on_stop_cb) {
-      type_.on_stop_cb(
-          &type_, args.internal_instance_index, type_.cb_user_arg, inst_ctx_,
-          const_cast<PerfettoDsOnStopArgs*>(
-              reinterpret_cast<const PerfettoDsOnStopArgs*>(&args)));
+      type_.on_stop_cb(&type_, args.internal_instance_index, type_.cb_user_arg,
+                       inst_ctx_, &c_args);
     }
 
-    std::lock_guard<std::mutex> lock(type_.mu);
-    type_.enabled_instances.reset(args.internal_instance_index);
-    if (type_.enabled_instances.none()) {
-      type_.enabled.store(false, std::memory_order_release);
+    // If c_args.stopper is nullptr, the user must have called
+    // PerfettoDsOnStopArgsPostpone() in the callback above: the user will
+    // invoke PerfettoDsStopDone later. If c_args.stopper is not nullptr, we
+    // need to invoke it.
+    if (c_args.stopper) {
+      PerfettoDsStopDone(c_args.stopper);
     }
   }
 
@@ -353,16 +379,14 @@
 
 PerfettoDsAsyncStopper* PerfettoDsOnStopArgsPostpone(
     PerfettoDsOnStopArgs* args) {
-  auto* cb = new std::function<void()>();
-  *cb = reinterpret_cast<const ShlibDataSource::StopArgs*>(args)
-            ->HandleStopAsynchronously();
-  return reinterpret_cast<PerfettoDsAsyncStopper*>(cb);
+  PerfettoDsAsyncStopper* stopper = args->stopper;
+  args->stopper = nullptr;
+  return stopper;
 }
 
 void PerfettoDsStopDone(PerfettoDsAsyncStopper* stopper) {
-  auto* cb = reinterpret_cast<std::function<void()>*>(stopper);
-  (*cb)();
-  delete cb;
+  stopper->FinishStop();
+  delete stopper;
 }
 
 PerfettoDsAsyncFlusher* PerfettoDsOnFlushArgsPostpone(
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index d5c7d45..5092ada 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -882,9 +882,50 @@
   std::thread t([&]() { tracing_session.StopBlocking(); });
 
   stop_called.WaitForNotification();
+
+  PERFETTO_DS_TRACE(data_source_2, ctx) {
+    struct PerfettoDsRootTracePacket trace_packet;
+    PerfettoDsTracerPacketBegin(&ctx, &trace_packet);
+
+    {
+      struct perfetto_protos_TestEvent for_testing;
+      perfetto_protos_TracePacket_begin_for_testing(&trace_packet.msg,
+                                                    &for_testing);
+      {
+        struct perfetto_protos_TestEvent_TestPayload payload;
+        perfetto_protos_TestEvent_begin_payload(&for_testing, &payload);
+        perfetto_protos_TestEvent_TestPayload_set_cstr_str(&payload,
+                                                           "After stop");
+        perfetto_protos_TestEvent_end_payload(&for_testing, &payload);
+      }
+      perfetto_protos_TracePacket_end_for_testing(&trace_packet.msg,
+                                                  &for_testing);
+    }
+    PerfettoDsTracerPacketEnd(&ctx, &trace_packet);
+  }
+
   PerfettoDsStopDone(stopper);
 
   t.join();
+
+  PERFETTO_DS_TRACE(data_source_2, ctx) {
+    // After the postponed stop has been acknowledged, this should not be
+    // executed.
+    ADD_FAILURE();
+  }
+
+  std::vector<uint8_t> data = tracing_session.ReadBlocking();
+  EXPECT_THAT(
+      FieldView(data),
+      Contains(PbField(
+          perfetto_protos_Trace_packet_field_number,
+          MsgField(Contains(PbField(
+              perfetto_protos_TracePacket_for_testing_field_number,
+              MsgField(Contains(PbField(
+                  perfetto_protos_TestEvent_payload_field_number,
+                  MsgField(ElementsAre(PbField(
+                      perfetto_protos_TestEvent_TestPayload_str_field_number,
+                      StringField("After stop")))))))))))));
 }
 
 TEST_F(SharedLibDataSourceTest, FlushDone) {
diff --git a/src/trace_config_utils/BUILD.gn b/src/trace_config_utils/BUILD.gn
index 586b2de..090aa91 100644
--- a/src/trace_config_utils/BUILD.gn
+++ b/src/trace_config_utils/BUILD.gn
@@ -73,9 +73,12 @@
 if (enable_perfetto_ui) {
   wasm_lib("trace_config_utils_wasm") {
     name = "trace_config_utils"
+    sources = [ "wasm.cc" ]
     deps = [
-      ":main",
+      ":pb_to_txt",
+      ":txt_to_pb",
       "../../gn:default_deps",
+      "../../include/perfetto/ext/base:base",
     ]
   }
 }
diff --git a/src/trace_config_utils/wasm.cc b/src/trace_config_utils/wasm.cc
new file mode 100644
index 0000000..b7d3853
--- /dev/null
+++ b/src/trace_config_utils/wasm.cc
@@ -0,0 +1,85 @@
+/*
+ * 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 <emscripten/emscripten.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <string>
+
+#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_config_utils/pb_to_txt.h"
+#include "src/trace_config_utils/txt_to_pb.h"
+
+namespace {
+// The buffer used to exchange input and output arguments. We assume 16MB
+// is enough to handle trace configs.
+char wasm_buf[16 * 1024 * 1024];
+}  // namespace
+
+extern "C" {
+
+// Returns the pointer to the buffer.
+void* EMSCRIPTEN_KEEPALIVE trace_config_utils_buf();
+void* trace_config_utils_buf() {
+  return &wasm_buf;
+}
+
+// Returns the size of the buffer, so trace_config_utils_wasm.ts doesn't have
+// to hardcode the 16MB.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_utils_buf_size();
+uint32_t trace_config_utils_buf_size() {
+  return static_cast<uint32_t>(sizeof(wasm_buf));
+}
+
+// Converts a proto-encoded protos.TraceConfig to text.
+// The caller must memcpy the bytes into the wasm_buf and pass the size of the
+// copied data into `size`. The returned pbtxt will be written in wasm_buf and
+// its size returned here.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_pb_to_txt(uint32_t size);
+
+uint32_t trace_config_pb_to_txt(uint32_t size) {
+  std::string txt = perfetto::TraceConfigPbToTxt(wasm_buf, size);
+  size_t wsize = perfetto::base::SprintfTrunc(wasm_buf, sizeof(wasm_buf), "%s",
+                                              txt.c_str());
+  return static_cast<uint32_t>(wsize);
+}
+
+// Like the above, but converts a pbtxt into proto-encoded bytes.
+// Because this can fail (the C++ function returns a StatusOr) we write
+// a success/failure in the first byte to tell the diffrence.
+uint32_t EMSCRIPTEN_KEEPALIVE trace_config_txt_to_pb(uint32_t size);
+
+uint32_t trace_config_txt_to_pb(uint32_t size) {
+  auto res = perfetto::TraceConfigTxtToPb(std::string(wasm_buf, size));
+  if (!res.ok()) {
+    wasm_buf[0] = 0;
+    size_t wsize = perfetto::base::SprintfTrunc(
+        &wasm_buf[1], sizeof(wasm_buf) - 1, "%s", res.status().c_message());
+    return static_cast<uint32_t>(wsize);
+  }
+  const size_t resp_size = std::min(res->size(), sizeof(wasm_buf) - 1);
+  wasm_buf[0] = 1;
+  memcpy(&wasm_buf[1], res->data(), resp_size);
+  return static_cast<uint32_t>(resp_size);
+}
+}  // extern "C"
+
+// This is unused but is needed to keep emscripten happy.
+int main(int, char**) {
+  return 0;
+}
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 7df41eb..0b86a1b 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -127,9 +127,6 @@
     "importers/ftrace:minimal",
     "importers/fuchsia:fuchsia_record",
     "importers/memory_tracker:graph_processor",
-    "importers/proto:gen_cc_android_track_event_descriptor",
-    "importers/proto:gen_cc_chrome_track_event_descriptor",
-    "importers/proto:gen_cc_track_event_descriptor",
     "importers/proto:minimal",
     "importers/systrace:systrace_line",
     "sorter",
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 840564a..258123d 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -75,10 +75,15 @@
   uint32_t rm_last = rm->Get(rm_size - 1);
   uint32_t range_size = rm_last - rm_first;
 
+  // Always prefer linear search if on a range *except* when the range is small
+  // but the last element of the range is large: this will cause a big bitvector
+  // to be created which negates the benefits of using linear search over
+  // index search.
+  bool disallows_index_search = rm->IsRange() && rm_last < range_size * 100;
+
   // If the number of elements in the rowmap is small or the number of
   // elements is less than 1/10th of the range, use indexed filtering.
   // TODO(b/283763282): use Overlay estimations.
-  bool disallows_index_search = rm->IsRange();
   bool prefers_index_search =
       rm->IsIndexVector() || rm_size < 1024 || rm_size * 10 < range_size;
 
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 6969793..3213417 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -42,8 +42,6 @@
 namespace {
 
 using SliceTable = tables::SliceTable;
-using ExpectedFrameTimelineSliceTable = tables::ExpectedFrameTimelineSliceTable;
-using RawTable = tables::RawTable;
 using FtraceEventTable = tables::FtraceEventTable;
 using HeapGraphObjectTable = tables::HeapGraphObjectTable;
 
@@ -51,10 +49,6 @@
 constexpr std::string_view kSliceTable =
     "test/data/slice_table_for_benchmarks.csv";
 
-// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
-constexpr std::string_view kExpectedFrameTimelineTable =
-    "test/data/expected_frame_timeline_for_benchmarks.csv";
-
 // `SELECT id, cpu FROM raw` on chrome_android_systrace.pftrace.
 constexpr std::string_view kRawTable = "test/data/raw_cpu_for_benchmarks.csv";
 
@@ -151,47 +145,6 @@
   SliceTable table_;
 };
 
-struct ExpectedFrameTimelineTableForBenchmark {
-  explicit ExpectedFrameTimelineTableForBenchmark(benchmark::State& state)
-      : table_{&pool_, &parent_} {
-    std::vector<std::string> table_rows_as_string =
-        ReadCSV(state, kExpectedFrameTimelineTable);
-    std::vector<std::string> parent_rows_as_string =
-        ReadCSV(state, kSliceTable);
-
-    uint32_t cur_idx = 0;
-    for (size_t i = 1; i < table_rows_as_string.size(); ++i, ++cur_idx) {
-      std::vector<std::string> row_vec = SplitCSVLine(table_rows_as_string[i]);
-
-      uint32_t idx = *base::StringToUInt32(row_vec[0]);
-      while (cur_idx < idx) {
-        parent_.Insert(
-            GetSliceTableRow(parent_rows_as_string[cur_idx + 1], pool_));
-        cur_idx++;
-      }
-
-      ExpectedFrameTimelineSliceTable::Row row;
-      row.ts = *base::StringToInt64(row_vec[2]);
-      row.dur = *base::StringToInt64(row_vec[3]);
-      row.track_id = tables::TrackTable::Id(*base::StringToUInt32(row_vec[4]));
-      row.depth = *base::StringToUInt32(row_vec[7]);
-      row.stack_id = *base::StringToInt32(row_vec[8]);
-      row.parent_stack_id = *base::StringToInt32(row_vec[9]);
-      row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
-                          ? std::make_optional<SliceTable::Id>(
-                                *base::StringToUInt32(row_vec[11]))
-                          : std::nullopt;
-      row.arg_set_id = *base::StringToUInt32(row_vec[11]);
-      row.thread_ts = base::StringToInt64(row_vec[12]);
-      row.thread_dur = base::StringToInt64(row_vec[13]);
-      table_.Insert(row);
-    }
-  }
-  StringPool pool_;
-  SliceTable parent_{&pool_};
-  ExpectedFrameTimelineSliceTable table_;
-};
-
 struct FtraceEventTableForBenchmark {
   explicit FtraceEventTableForBenchmark(benchmark::State& state) {
     std::vector<std::string> raw_rows = ReadCSV(state, kRawTable);
@@ -201,14 +154,6 @@
     uint32_t cur_idx = 0;
     for (size_t i = 1; i < ftrace_event_rows.size(); ++i, cur_idx++) {
       std::vector<std::string> row_vec = SplitCSVLine(ftrace_event_rows[i]);
-      uint32_t idx = *base::StringToUInt32(row_vec[0]);
-      while (cur_idx < idx) {
-        std::vector<std::string> raw_row = SplitCSVLine(raw_rows[cur_idx + 1]);
-        RawTable::Row r;
-        r.ucpu = tables::CpuTable::Id(*base::StringToUInt32(raw_row[1]));
-        raw_.Insert(r);
-        cur_idx++;
-      }
       FtraceEventTable::Row row;
       row.ucpu = tables::CpuTable::Id(*base::StringToUInt32(row_vec[1]));
       table_.Insert(row);
@@ -216,8 +161,7 @@
   }
 
   StringPool pool_;
-  RawTable raw_{&pool_};
-  tables::FtraceEventTable table_{&pool_, &raw_};
+  tables::FtraceEventTable table_{&pool_};
 };
 
 struct HeapGraphObjectTableForBenchmark {
@@ -268,23 +212,6 @@
                              benchmark::Counter::kInvert);
 }
 
-void BenchmarkExpectedFrameTableQuery(
-    benchmark::State& state,
-    ExpectedFrameTimelineTableForBenchmark& table,
-    Query q) {
-  for (auto _ : state) {
-    benchmark::DoNotOptimize(table.table_.FilterToIterator(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(CountRows(table.table_.FilterToIterator(q)),
-                         benchmark::Counter::kIsIterationInvariantRate |
-                             benchmark::Counter::kInvert);
-}
-
 void BenchmarkFtraceEventTableQuery(benchmark::State& state,
                                     FtraceEventTableForBenchmark& table,
                                     Query q) {
@@ -370,14 +297,6 @@
 }
 BENCHMARK(BM_QESliceTableSorted);
 
-void BM_QEFilterWithSparseSelector(benchmark::State& state) {
-  ExpectedFrameTimelineTableForBenchmark table(state);
-  Query q;
-  q.constraints = {table.table_.track_id().eq(1445)};
-  BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEFilterWithSparseSelector);
-
 void BM_QEFilterWithDenseSelector(benchmark::State& state) {
   FtraceEventTableForBenchmark table(state);
   Query q;
@@ -585,15 +504,6 @@
 }
 BENCHMARK(BM_QEFtraceEventSortSelectorNumericDesc);
 
-void BM_QEDistinctWithSparseSelector(benchmark::State& state) {
-  ExpectedFrameTimelineTableForBenchmark table(state);
-  Query q;
-  q.order_type = Query::OrderType::kDistinct;
-  q.orders = {table.table_.track_id().descending()};
-  BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEDistinctWithSparseSelector);
-
 void BM_QEDistinctWithDenseSelector(benchmark::State& state) {
   FtraceEventTableForBenchmark table(state);
   Query q;
@@ -603,15 +513,6 @@
 }
 BENCHMARK(BM_QEDistinctWithDenseSelector);
 
-void BM_QEDistinctSortedWithSparseSelector(benchmark::State& state) {
-  ExpectedFrameTimelineTableForBenchmark table(state);
-  Query q;
-  q.order_type = Query::OrderType::kDistinctAndSort;
-  q.orders = {table.table_.track_id().descending()};
-  BenchmarkExpectedFrameTableQuery(state, table, q);
-}
-BENCHMARK(BM_QEDistinctSortedWithSparseSelector);
-
 void BM_QEDistinctSortedWithDenseSelector(benchmark::State& state) {
   FtraceEventTableForBenchmark table(state);
   Query q;
diff --git a/src/trace_processor/export_json.cc b/src/trace_processor/export_json.cc
index 760f058..845298b 100644
--- a/src/trace_processor/export_json.cc
+++ b/src/trace_processor/export_json.cc
@@ -37,6 +37,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
@@ -503,28 +504,24 @@
           inf_value_(Json::StaticString("Infinity")),
           neg_inf_value_(Json::StaticString("-Infinity")) {
       const auto& arg_table = storage_->arg_table();
-      uint32_t count = arg_table.row_count();
-      if (count == 0) {
-        args_sets_.resize(1, empty_value_);
-        return;
-      }
-      args_sets_.resize(arg_table[count - 1].arg_set_id() + 1, empty_value_);
-
+      Json::Value* cur_args_ptr = nullptr;
+      uint32_t cur_args_set_id = std::numeric_limits<uint32_t>::max();
       for (auto it = arg_table.IterateRows(); it; ++it) {
         ArgSetId set_id = it.arg_set_id();
+        if (set_id != cur_args_set_id) {
+          cur_args_ptr =
+              args_sets_.Insert(set_id, Json::Value(Json::objectValue)).first;
+          cur_args_set_id = set_id;
+        }
         const char* key = storage->GetString(it.key()).c_str();
         Variadic value = storage_->GetArgValue(it.row_number().row_number());
-        AppendArg(set_id, key, VariadicToJson(value));
+        AppendArg(cur_args_ptr, key, VariadicToJson(value));
       }
       PostprocessArgs();
     }
 
-    const Json::Value& GetArgs(ArgSetId set_id) const {
-      // If |set_id| was empty and added to the storage last, it may not be in
-      // args_sets_.
-      if (set_id > args_sets_.size())
-        return empty_value_;
-      return args_sets_[set_id];
+    const Json::Value& GetArgs(std::optional<ArgSetId> set_id) const {
+      return set_id ? *args_sets_.Find(*set_id) : empty_value_;
     }
 
    private:
@@ -566,15 +563,13 @@
       PERFETTO_FATAL("Not reached");  // For gcc.
     }
 
-    void AppendArg(ArgSetId set_id,
-                   const std::string& key,
-                   const Json::Value& value) {
-      Json::Value* target = &args_sets_[set_id];
+    static void AppendArg(Json::Value* target,
+                          const std::string& key,
+                          const Json::Value& value) {
       for (base::StringSplitter parts(key, '.'); parts.Next();) {
         if (PERFETTO_UNLIKELY(!target->isNull() && !target->isObject())) {
           PERFETTO_DLOG("Malformed arguments. Can't append %s to %s.",
-                        key.c_str(),
-                        args_sets_[set_id].toStyledString().c_str());
+                        key.c_str(), target->toStyledString().c_str());
           return;
         }
         std::string key_part = parts.cur_token();
@@ -592,8 +587,7 @@
                                                     bracketpos - 1);
             if (PERFETTO_UNLIKELY(!target->isNull() && !target->isArray())) {
               PERFETTO_DLOG("Malformed arguments. Can't append %s to %s.",
-                            key.c_str(),
-                            args_sets_[set_id].toStyledString().c_str());
+                            key.c_str(), target->toStyledString().c_str());
               return;
             }
             std::optional<uint32_t> index = base::StringToUInt32(s);
@@ -611,7 +605,8 @@
     }
 
     void PostprocessArgs() {
-      for (Json::Value& args : args_sets_) {
+      for (auto it = args_sets_.GetIterator(); it; ++it) {
+        auto& args = it.value();
         // Move all fields from "debug" key to upper level.
         if (args.isMember("debug")) {
           Json::Value debug = args["debug"];
@@ -648,7 +643,7 @@
     }
 
     const TraceStorage* storage_;
-    std::vector<Json::Value> args_sets_;
+    base::FlatHashMap<ArgSetId, Json::Value> args_sets_;
     const Json::Value empty_value_;
     const Json::Value nan_value_;
     const Json::Value inf_value_;
@@ -1073,12 +1068,12 @@
     for (auto it = flow_table.IterateRows(); it; ++it) {
       SliceId slice_out = it.slice_out();
       SliceId slice_in = it.slice_in();
-      uint32_t arg_set_id = it.arg_set_id();
+      std::optional<uint32_t> arg_set_id = it.arg_set_id();
 
       std::string cat;
       std::string name;
       auto args = args_builder_.GetArgs(arg_set_id);
-      if (arg_set_id != kInvalidArgSetId) {
+      if (arg_set_id != std::nullopt) {
         cat = args["cat"].asString();
         name = args["name"].asString();
         // Don't export these args since they are only used for this export and
@@ -1107,7 +1102,7 @@
   }
 
   Json::Value ConvertLegacyRawEventToJson(
-      const tables::RawTable::ConstIterator& it) {
+      const tables::ChromeRawTable::ConstIterator& it) {
     Json::Value event;
     event["ts"] = Json::Int64(it.ts() / 1000);
 
@@ -1192,7 +1187,7 @@
     std::optional<StringId> raw_chrome_metadata_event_id =
         storage_->string_pool().GetId("chrome_event.metadata");
 
-    const auto& events = storage_->raw_table();
+    const auto& events = storage_->chrome_raw_table();
     for (auto it = events.IterateRows(); it; ++it) {
       if (raw_legacy_event_key_id && it.name() == *raw_legacy_event_key_id) {
         Json::Value event = ConvertLegacyRawEventToJson(it);
diff --git a/src/trace_processor/export_json.h b/src/trace_processor/export_json.h
index 47726bf..34f0973 100644
--- a/src/trace_processor/export_json.h
+++ b/src/trace_processor/export_json.h
@@ -28,10 +28,10 @@
 namespace json {
 
 // Export trace to a file stream in json format.
-util::Status ExportJson(const TraceStorage*, FILE* output);
+base::Status ExportJson(const TraceStorage*, FILE* output);
 
 // For testing.
-util::Status ExportJson(const TraceStorage* storage,
+base::Status ExportJson(const TraceStorage* storage,
                         OutputWriter*,
                         ArgumentFilterPredicate = nullptr,
                         MetadataFilterPredicate = nullptr,
diff --git a/src/trace_processor/export_json_unittest.cc b/src/trace_processor/export_json_unittest.cc
index aa46048..dd009d1 100644
--- a/src/trace_processor/export_json_unittest.cc
+++ b/src/trace_processor/export_json_unittest.cc
@@ -155,8 +155,8 @@
   StringId name_id = context_.storage->InternString(base::StringView(kName));
   // The thread_slice table is a sub table of slice.
   context_.storage->mutable_slice_table()->Insert(
-      {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u), 0,
-       kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
+      {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u),
+       std::nullopt, kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
        kThreadInstructionDelta});
 
   base::TempFile temp_file = base::TempFile::Create();
@@ -180,7 +180,7 @@
   EXPECT_EQ(event["cat"].asString(), kCategory);
   EXPECT_EQ(event["name"].asString(), kName);
   EXPECT_TRUE(event["args"].isObject());
-  EXPECT_EQ(event["args"].size(), 0u);
+  EXPECT_EQ(event["args"].size(), 0u) << event["args"].toStyledString();
 }
 
 TEST_F(ExportJsonTest, StorageWithOneUnfinishedSlice) {
@@ -200,8 +200,8 @@
   StringId cat_id = context_.storage->InternString(base::StringView(kCategory));
   StringId name_id = context_.storage->InternString(base::StringView(kName));
   context_.storage->mutable_slice_table()->Insert(
-      {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u), 0,
-       kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
+      {kTimestamp, kDuration, track, cat_id, name_id, 0, 0, 0, SliceId(0u),
+       std::nullopt, kThreadTimestamp, kThreadDuration, kThreadInstructionCount,
        kThreadInstructionDelta});
 
   base::TempFile temp_file = base::TempFile::Create();
@@ -414,11 +414,10 @@
 
   TraceStorage* storage = context_.storage.get();
 
-  auto ucpu = context_.cpu_tracker->GetOrCreateCpu(0);
-  RawId id = storage->mutable_raw_table()
-                 ->Insert({0, storage->InternString("chrome_event.metadata"), 0,
-                           0, 0, ucpu})
-                 .id;
+  tables::ChromeRawTable::Id id =
+      storage->mutable_chrome_raw_table()
+          ->Insert({0, storage->InternString("chrome_event.metadata"), 0, 0})
+          .id;
 
   StringId name1_id = storage->InternString(base::StringView(kName1));
   StringId name2_id = storage->InternString(base::StringView(kName2));
@@ -780,7 +779,6 @@
             context_.storage->InternString("source"),
             Variadic::String(context_.storage->InternString("chrome")));
       });
-  context_.args_tracker->Flush();  // Flush track args.
   StringId cat_id = context_.storage->InternString(base::StringView(kCategory));
   StringId name_id = context_.storage->InternString(base::StringView(kName));
   context_.storage->mutable_slice_table()->Insert(
@@ -788,8 +786,8 @@
 
   // Global track.
   TrackEventTracker track_event_tracker(&context_);
-  TrackId track2 = track_event_tracker.GetOrCreateDefaultDescriptorTrack();
-  context_.args_tracker->Flush();  // Flush track args.
+  TrackId track2 = *track_event_tracker.GetDescriptorTrack(
+      TrackEventTracker::kDefaultDescriptorTrackUuid);
   context_.storage->mutable_slice_table()->Insert(
       {kTimestamp2, 0, track2, cat_id, name_id, 0, 0, 0});
 
@@ -798,7 +796,6 @@
   reservation.parent_uuid = 0;
   track_event_tracker.ReserveDescriptorTrack(1234, reservation);
   TrackId track3 = *track_event_tracker.GetDescriptorTrack(1234);
-  context_.args_tracker->Flush();  // Flush track args.
   context_.storage->mutable_slice_table()->Insert(
       {kTimestamp3, 0, track3, cat_id, name_id, 0, 0, 0});
 
@@ -1428,10 +1425,8 @@
   auto& tt = *context_.storage->mutable_thread_table();
   tt[utid].set_upid(upid);
 
-  auto ucpu = context_.cpu_tracker->GetOrCreateCpu(0);
-  auto id_and_row = storage->mutable_raw_table()->Insert(
-      {kTimestamp, storage->InternString("track_event.legacy_event"), utid, 0,
-       0, ucpu});
+  auto id_and_row = storage->mutable_chrome_raw_table()->Insert(
+      {kTimestamp, storage->InternString("track_event.legacy_event"), utid, 0});
   auto inserter = context_.args_tracker->AddArgsTo(id_and_row.id);
 
   auto add_arg = [&](const char* key, Variadic value) {
@@ -1500,7 +1495,7 @@
   const char* kLegacyJsonData2 = "er\": 1},{\"user\": 2}";
 
   TraceStorage* storage = context_.storage.get();
-  auto* raw = storage->mutable_raw_table();
+  auto* raw = storage->mutable_chrome_raw_table();
 
   auto id_and_row = raw->Insert(
       {0, storage->InternString("chrome_event.legacy_system_trace"), 0, 0});
@@ -1726,8 +1721,8 @@
 
   TraceStorage* storage = context_.storage.get();
 
-  auto* raw = storage->mutable_raw_table();
-  RawId id =
+  auto* raw = storage->mutable_chrome_raw_table();
+  tables::ChromeRawTable::Id id =
       raw->Insert({0, storage->InternString("chrome_event.metadata"), 0, 0}).id;
 
   StringId name1_id = storage->InternString(base::StringView(kName1));
diff --git a/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc b/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
index a719a15..c8c9816 100644
--- a/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_battery_stats_reader.cc
@@ -58,7 +58,7 @@
 
 AndroidBatteryStatsReader::~AndroidBatteryStatsReader() = default;
 
-util::Status AndroidBatteryStatsReader::ParseLine(base::StringView line) {
+base::Status AndroidBatteryStatsReader::ParseLine(base::StringView line) {
   base::StringViewSplitter splitter(line, ',');
 
   // consume the legacy version number which we expect to be at the start of
@@ -131,7 +131,7 @@
   return base::OkStatus();
 }
 
-util::Status AndroidBatteryStatsReader::ProcessBatteryStatsHistoryEvent(
+base::Status AndroidBatteryStatsReader::ProcessBatteryStatsHistoryEvent(
     base::StringView raw_event) {
   AndroidDumpstateEvent event{
       AndroidDumpstateEvent::EventType::kBatteryStatsHistoryEvent,
@@ -139,7 +139,7 @@
   return SendToSorter(std::chrono::milliseconds(current_timestamp_ms_), event);
 }
 
-util::Status AndroidBatteryStatsReader::SendToSorter(
+base::Status AndroidBatteryStatsReader::SendToSorter(
     std::chrono::nanoseconds event_ts,
     AndroidDumpstateEvent event) {
   ASSIGN_OR_RETURN(
diff --git a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
index 07c8434..2e28fb7 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.cc
@@ -95,7 +95,7 @@
 }
 
 // static
-util::Status AndroidBugreportReader::Parse(TraceProcessorContext* context,
+base::Status AndroidBugreportReader::Parse(TraceProcessorContext* context,
                                            std::vector<util::ZipFile> files) {
   auto res = FindBugReportFile(files);
   if (!res.has_value()) {
@@ -136,7 +136,7 @@
 
 AndroidBugreportReader::~AndroidBugreportReader() = default;
 
-util::Status AndroidBugreportReader::ParseImpl() {
+base::Status AndroidBugreportReader::ParseImpl() {
   // All logs in Android bugreports use wall time (which creates problems
   // in case of early boot events before NTP kicks in, which get emitted as
   // 1970), but that is the state of affairs.
diff --git a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
index b36c45c..5269a19 100644
--- a/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
+++ b/src/trace_processor/importers/android_bugreport/android_bugreport_reader.h
@@ -42,7 +42,7 @@
  public:
   static bool IsAndroidBugReport(
       const std::vector<util::ZipFile>& zip_file_entries);
-  static util::Status Parse(TraceProcessorContext* context,
+  static base::Status Parse(TraceProcessorContext* context,
                             std::vector<util::ZipFile> zip_file_entries);
 
  private:
@@ -70,7 +70,7 @@
                          BugReportFile bug_report,
                          std::set<LogFile> ordered_log_files);
   ~AndroidBugreportReader();
-  util::Status ParseImpl();
+  base::Status ParseImpl();
 
   base::StatusOr<std::vector<TimestampedAndroidLogEvent>>
   ParsePersistentLogcat();
diff --git a/src/trace_processor/importers/android_bugreport/android_log_reader.cc b/src/trace_processor/importers/android_bugreport/android_log_reader.cc
index 740538d..ea0fbe1 100644
--- a/src/trace_processor/importers/android_bugreport/android_log_reader.cc
+++ b/src/trace_processor/importers/android_bugreport/android_log_reader.cc
@@ -128,7 +128,7 @@
 
 AndroidLogReader::~AndroidLogReader() = default;
 
-util::Status AndroidLogReader::ParseLine(base::StringView line) {
+base::Status AndroidLogReader::ParseLine(base::StringView line) {
   if (line.size() < 30 ||
       (line.at(0) == '-' && line.at(1) == '-' && line.at(2) == '-')) {
     // These are markers like "--------- switch to radio" which we ignore.
@@ -221,12 +221,12 @@
   return ProcessEvent(event_ts, std::move(event));
 }
 
-util::Status AndroidLogReader::ProcessEvent(std::chrono::nanoseconds event_ts,
+base::Status AndroidLogReader::ProcessEvent(std::chrono::nanoseconds event_ts,
                                             AndroidLogEvent event) {
   return SendToSorter(event_ts, std::move(event));
 }
 
-util::Status AndroidLogReader::SendToSorter(std::chrono::nanoseconds event_ts,
+base::Status AndroidLogReader::SendToSorter(std::chrono::nanoseconds event_ts,
                                             AndroidLogEvent event) {
   event_ts -= timezone_offset_;
   ASSIGN_OR_RETURN(
diff --git a/src/trace_processor/importers/archive/tar_trace_reader.cc b/src/trace_processor/importers/archive/tar_trace_reader.cc
index d0cf11f4..2ea8955 100644
--- a/src/trace_processor/importers/archive/tar_trace_reader.cc
+++ b/src/trace_processor/importers/archive/tar_trace_reader.cc
@@ -126,7 +126,7 @@
 
 TarTraceReader::~TarTraceReader() = default;
 
-util::Status TarTraceReader::Parse(TraceBlobView blob) {
+base::Status TarTraceReader::Parse(TraceBlobView blob) {
   ParseResult result = ParseResult::kOk;
   buffer_.PushBack(std::move(blob));
   while (!buffer_.empty() && result == ParseResult::kOk) {
diff --git a/src/trace_processor/importers/common/args_tracker.h b/src/trace_processor/importers/common/args_tracker.h
index bacd67b..dd1b645 100644
--- a/src/trace_processor/importers/common/args_tracker.h
+++ b/src/trace_processor/importers/common/args_tracker.h
@@ -21,6 +21,7 @@
 #include "perfetto/ext/base/small_vector.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
@@ -104,8 +105,12 @@
 
   virtual ~ArgsTracker();
 
-  BoundInserter AddArgsTo(RawId id) {
-    return AddArgsTo(context_->storage->mutable_raw_table(), id);
+  BoundInserter AddArgsTo(tables::ChromeRawTable::Id id) {
+    return AddArgsTo(context_->storage->mutable_chrome_raw_table(), id);
+  }
+
+  BoundInserter AddArgsTo(tables::FtraceEventTable::Id id) {
+    return AddArgsTo(context_->storage->mutable_ftrace_event_table(), id);
   }
 
   BoundInserter AddArgsTo(CounterId id) {
diff --git a/src/trace_processor/importers/common/flow_tracker.cc b/src/trace_processor/importers/common/flow_tracker.cc
index 4d6cc66..6990e2a 100644
--- a/src/trace_processor/importers/common/flow_tracker.cc
+++ b/src/trace_processor/importers/common/flow_tracker.cc
@@ -142,8 +142,7 @@
 void FlowTracker::InsertFlow(FlowId flow_id,
                              SliceId slice_out_id,
                              SliceId slice_in_id) {
-  tables::FlowTable::Row row(slice_out_id, slice_in_id, flow_id,
-                             kInvalidArgSetId);
+  tables::FlowTable::Row row(slice_out_id, slice_in_id, flow_id, std::nullopt);
   auto id = context_->storage->mutable_flow_table()->Insert(row).id;
 
   auto* it = flow_id_to_v1_flow_id_map_.Find(flow_id);
@@ -158,7 +157,7 @@
 
 void FlowTracker::InsertFlow(SliceId slice_out_id, SliceId slice_in_id) {
   tables::FlowTable::Row row(slice_out_id, slice_in_id, std::nullopt,
-                             kInvalidArgSetId);
+                             std::nullopt);
   context_->storage->mutable_flow_table()->Insert(row);
 }
 
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index 2fd9f36..916ed2b 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -145,21 +145,18 @@
 
     auto& arg_table = *storage_->mutable_arg_table();
 
+    uint32_t arg_set_id = arg_table.row_count();
     ArgSetHash digest = hash.digest();
-    auto it_and_inserted =
-        arg_row_for_hash_.Insert(digest, arg_table.row_count());
-    if (!it_and_inserted.second) {
+    auto [it, inserted] = arg_row_for_hash_.Insert(digest, arg_set_id);
+    if (!inserted) {
       // Already inserted.
-      return arg_table[*it_and_inserted.first].arg_set_id();
+      return *it;
     }
 
-    // Taking size() after the Insert() ensures that nothing has an id == 0
-    // (0 == kInvalidArgSetId).
-    auto id = static_cast<uint32_t>(arg_row_for_hash_.size());
     for (const CompactArg* ptr : valid) {
       const auto& arg = *ptr;
       tables::ArgTable::Row row;
-      row.arg_set_id = id;
+      row.arg_set_id = arg_set_id;
       row.flat_key = arg.flat_key;
       row.key = arg.key;
       switch (arg.value.type) {
@@ -190,7 +187,7 @@
       row.value_type = storage_->GetIdForVariadicType(arg.value.type);
       arg_table.Insert(row);
     }
-    return id;
+    return arg_set_id;
   }
 
   base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
diff --git a/src/trace_processor/importers/common/jit_cache.cc b/src/trace_processor/importers/common/jit_cache.cc
index 8380177..452aaf2 100644
--- a/src/trace_processor/importers/common/jit_cache.cc
+++ b/src/trace_processor/importers/common/jit_cache.cc
@@ -108,6 +108,30 @@
   return jit_code_id;
 }
 
+tables::JitCodeTable::Id JitCache::MoveCode(int64_t timestamp,
+                                            UniqueTid,
+                                            uint64_t from_code_start,
+                                            uint64_t to_code_start) {
+  auto* jit_code_table = context_->storage->mutable_jit_code_table();
+
+  auto it = functions_.Find(from_code_start);
+  AddressRange old_code_range = it->first;
+  JittedFunction func = std::move(it->second);
+  functions_.erase(it);
+
+  auto code_id = func.jit_code_id();
+  AddressRange new_code_range(to_code_start, old_code_range.size());
+
+  functions_.DeleteOverlapsAndEmplace(
+      [&](std::pair<const AddressRange, JittedFunction>& entry) {
+        jit_code_table->FindById(entry.second.jit_code_id())
+            ->set_estimated_delete_ts(timestamp);
+      },
+      new_code_range, std::move(func));
+
+  return code_id;
+}
+
 std::pair<FrameId, bool> JitCache::InternFrame(VirtualMemoryMapping* mapping,
                                                uint64_t rel_pc,
                                                base::StringView function_name) {
diff --git a/src/trace_processor/importers/common/jit_cache.h b/src/trace_processor/importers/common/jit_cache.h
index 19205b6..4a7eaf2 100644
--- a/src/trace_processor/importers/common/jit_cache.h
+++ b/src/trace_processor/importers/common/jit_cache.h
@@ -68,6 +68,10 @@
       StringId function_name,
       std::optional<SourceLocation> source_location,
       TraceBlobView native_code);
+  tables::JitCodeTable::Id MoveCode(int64_t timestamp,
+                                    UniqueTid utid,
+                                    uint64_t from_code_start,
+                                    uint64_t to_code_start);
 
   // Forward frame interning request.
   // MappingTracker allows other trackers to register ranges of memory for
diff --git a/src/trace_processor/importers/common/parser_types.h b/src/trace_processor/importers/common/parser_types.h
index 917479a..07eb8df 100644
--- a/src/trace_processor/importers/common/parser_types.h
+++ b/src/trace_processor/importers/common/parser_types.h
@@ -23,6 +23,7 @@
 #include <optional>
 #include <string>
 #include <utility>
+#include <variant>
 
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
@@ -56,16 +57,19 @@
 static_assert(sizeof(InlineSchedWaking) == 16);
 
 struct alignas(8) JsonEvent {
+  struct Begin {};
+  struct End {};
+  struct Scoped {
+    int64_t dur;
+  };
+  struct Other {};
+  using Type = std::variant<Begin, End, Scoped, Other>;
+
   std::string value;
+  Type type;
 };
 static_assert(sizeof(JsonEvent) % 8 == 0);
 
-struct alignas(8) JsonWithDurEvent {
-  int64_t dur;
-  std::string value;
-};
-static_assert(sizeof(JsonWithDurEvent) % 8 == 0);
-
 struct alignas(8) TracePacketData {
   TraceBlobView packet;
   RefPtr<PacketSequenceStateGeneration> sequence_state;
diff --git a/src/trace_processor/importers/common/slice_tracker_unittest.cc b/src/trace_processor/importers/common/slice_tracker_unittest.cc
index b999ca1..ba73cd7 100644
--- a/src/trace_processor/importers/common/slice_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/slice_tracker_unittest.cc
@@ -91,7 +91,7 @@
   EXPECT_EQ(slices[0].category().value_or(kNullStringId).raw_id(), 0u);
   EXPECT_EQ(slices[0].name().value_or(kNullStringId).raw_id(), 1u);
   EXPECT_EQ(slices[0].depth(), 0u);
-  EXPECT_EQ(slices[0].arg_set_id(), kInvalidArgSetId);
+  EXPECT_EQ(slices[0].arg_set_id(), std::nullopt);
 }
 
 TEST_F(SliceTrackerTest, OneSliceDetailedWithTranslatedName) {
@@ -115,7 +115,7 @@
   EXPECT_EQ(slices[0].name().value_or(kNullStringId).raw_id(),
             mapped_name.raw_id());
   EXPECT_EQ(slices[0].depth(), 0u);
-  EXPECT_EQ(slices[0].arg_set_id(), kInvalidArgSetId);
+  EXPECT_EQ(slices[0].arg_set_id(), std::nullopt);
 }
 
 TEST_F(SliceTrackerTest, NegativeTimestamps) {
@@ -137,7 +137,7 @@
   EXPECT_EQ(rr.category().value_or(kNullStringId).raw_id(), 0u);
   EXPECT_EQ(rr.name().value_or(kNullStringId).raw_id(), 1u);
   EXPECT_EQ(rr.depth(), 0u);
-  EXPECT_EQ(rr.arg_set_id(), kInvalidArgSetId);
+  EXPECT_EQ(rr.arg_set_id(), std::nullopt);
 }
 
 TEST_F(SliceTrackerTest, OneSliceWithArgs) {
diff --git a/src/trace_processor/importers/common/tracks.h b/src/trace_processor/importers/common/tracks.h
index 22f0297..aeda1bb 100644
--- a/src/trace_processor/importers/common/tracks.h
+++ b/src/trace_processor/importers/common/tracks.h
@@ -158,7 +158,7 @@
 }
 
 // Indicates that the name of the track was provided in the blueprint.
-constexpr nullptr_t BlueprintName() {
+constexpr std::nullptr_t BlueprintName() {
   return nullptr;
 }
 
@@ -169,7 +169,7 @@
 }
 
 // Indicates that the unit of the track was provided in the blueprint.
-constexpr nullptr_t BlueprintUnit() {
+constexpr std::nullptr_t BlueprintUnit() {
   return nullptr;
 }
 
diff --git a/src/trace_processor/importers/common/tracks_internal.h b/src/trace_processor/importers/common/tracks_internal.h
index 5cc0fa1..680cdfb 100644
--- a/src/trace_processor/importers/common/tracks_internal.h
+++ b/src/trace_processor/importers/common/tracks_internal.h
@@ -42,17 +42,17 @@
 
 struct NameBlueprintT {
   struct Auto {
-    using name_t = nullptr_t;
+    using name_t = std::nullptr_t;
   };
   struct Static {
-    using name_t = nullptr_t;
+    using name_t = std::nullptr_t;
     const char* name;
   };
   struct Dynamic {
     using name_t = StringPool::Id;
   };
   struct FnBase {
-    using name_t = nullptr_t;
+    using name_t = std::nullptr_t;
   };
   template <typename F>
   struct Fn : FnBase {
@@ -84,7 +84,7 @@
 
 struct UnitBlueprintT {
   struct Unknown {
-    using unit_t = nullptr_t;
+    using unit_t = std::nullptr_t;
   };
   struct Static {
     using unit_t = const char*;
diff --git a/src/trace_processor/importers/etm/BUILD.gn b/src/trace_processor/importers/etm/BUILD.gn
index 7b03fcf..4f15044 100644
--- a/src/trace_processor/importers/etm/BUILD.gn
+++ b/src/trace_processor/importers/etm/BUILD.gn
@@ -63,6 +63,7 @@
     "mapping_version.cc",
     "mapping_version.h",
     "opencsd.h",
+    "sql_values.h",
     "storage_handle.cc",
     "storage_handle.h",
     "target_memory.cc",
diff --git a/src/trace_processor/importers/etm/element_cursor.cc b/src/trace_processor/importers/etm/element_cursor.cc
index 9366643..cc27040 100644
--- a/src/trace_processor/importers/etm/element_cursor.cc
+++ b/src/trace_processor/importers/etm/element_cursor.cc
@@ -23,6 +23,8 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "src/trace_processor/importers/etm/etm_v4_decoder.h"
+#include "src/trace_processor/importers/etm/mapping_version.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
 #include "src/trace_processor/importers/etm/storage_handle.h"
 #include "src/trace_processor/importers/etm/target_memory.h"
 #include "src/trace_processor/importers/etm/target_memory_reader.h"
@@ -131,4 +133,24 @@
   return OCSD_RESP_WAIT;
 }
 
+std::unique_ptr<InstructionRangeSqlValue> ElementCursor::GetInstructionRange()
+    const {
+  auto r = std::make_unique<InstructionRangeSqlValue>();
+  AddressRange range(element_->st_addr, element_->en_addr);
+  r->config_id = *config_id_;
+  r->isa = element_->isa;
+  r->st_addr = range.start();
+  // How did we get a range if there is no mapping.
+  PERFETTO_CHECK(mapping_);
+
+  if (!mapping_->Contains(range) || !mapping_->data()) {
+    r->start = nullptr;
+    r->end = nullptr;
+  } else {
+    r->start = mapping_->data() + (range.start() - mapping_->start());
+    r->end = r->start + range.size();
+  }
+  return r;
+}
+
 }  // namespace perfetto::trace_processor::etm
diff --git a/src/trace_processor/importers/etm/element_cursor.h b/src/trace_processor/importers/etm/element_cursor.h
index 9dabf9a..aa5e559 100644
--- a/src/trace_processor/importers/etm/element_cursor.h
+++ b/src/trace_processor/importers/etm/element_cursor.h
@@ -33,6 +33,7 @@
 class MappingVersion;
 class TargetMemory;
 class TargetMemoryReader;
+struct InstructionRangeSqlValue;
 
 class ElementTypeMask {
  public:
@@ -96,6 +97,12 @@
 
   const MappingVersion* mapping() const { return mapping_; }
 
+  bool has_instruction_range() const {
+    return element_->getType() == OCSD_GEN_TRC_ELEM_INSTR_RANGE;
+  }
+
+  std::unique_ptr<InstructionRangeSqlValue> GetInstructionRange() const;
+
  private:
   void SetAtEof();
   base::Status ResetDecoder(tables::EtmV4ConfigurationTable::Id config_id);
diff --git a/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc b/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
index acff104..f24dfc4 100644
--- a/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
+++ b/src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.cc
@@ -225,13 +225,27 @@
 // extract the various streams. AUX data is passed by the perf importer to the
 // CPU specific `AuxDataStream`, but as we just said we need to first decode
 // this data to extract the real per CPU streams, so the `EtmV4Stream` classes
-// (`AuxDataStream` subclasses) forward such data tho this class, that will
+// (`AuxDataStream` subclasses) forward such data to this class, that will
 // decode the streams and finally forward them back to the CPU specific
 // `EtmV4Stream` where it can now be handled.
 //
 // For the TRBE the data that arrives in the AUX record is unformatted and is
 // the data for that given CPU so it can be directly processed by the
 // `EtmV4Stream` class without needing to decode it first.
+//
+// Data flow for framed data (ETR):
+//   1. `PerfDataTokenizer` parses `AuxData` for cpu x and forwards it to the
+//      `AuxDataStream` bound to that cpu
+//   2. `EtmV4Stream` bound to cpu x determines AuxData is framed and forwards
+//       it to the `FrameDecoder` owned by `EtmV4StreamDemultiplexer`.
+//   3. De-multiplexed ETM data is sent to its corresponding `EtmV4Stream` where
+//      it is stored in `TraceStorage`.
+//
+// Data flow for raw data (TRBE):
+//   1. `PerfDataTokenizer` parses `AuxData` for cpu x and forwards it to the
+//      `AuxDataStream` bound to that cpu
+//   2. `EtmV4Stream` bound to cpu x determines AuxData is raw and can directly
+//      store it in `TraceStorage`.
 class EtmV4StreamDemultiplexer : public perf_importer::AuxDataTokenizer {
  public:
   explicit EtmV4StreamDemultiplexer(TraceProcessorContext* context)
diff --git a/src/trace_processor/importers/etm/mapping_version.h b/src/trace_processor/importers/etm/mapping_version.h
index 971daef..5333845 100644
--- a/src/trace_processor/importers/etm/mapping_version.h
+++ b/src/trace_processor/importers/etm/mapping_version.h
@@ -49,8 +49,6 @@
     return content_.data();
   }
 
-  const uint8_t* FindData(AddressRange range) const;
-
   MappingVersion SplitFront(uint64_t mid);
 
  private:
diff --git a/src/trace_processor/importers/etm/sql_values.h b/src/trace_processor/importers/etm/sql_values.h
new file mode 100644
index 0000000..1ed71d8
--- /dev/null
+++ b/src/trace_processor/importers/etm/sql_values.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/tables/etm_tables_py.h"
+
+namespace perfetto::trace_processor::etm {
+
+struct InstructionRangeSqlValue {
+  static constexpr const char kPtrType[] = "etm::InstructionRangeSqlValue";
+  tables::EtmV4ConfigurationTable::Id config_id;
+  ocsd_isa isa;
+  uint64_t st_addr;
+  const uint8_t* start;
+  const uint8_t* end;
+};
+
+}  // namespace perfetto::trace_processor::etm
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ETM_SQL_VALUES_H_
diff --git a/src/trace_processor/importers/etm/virtual_address_space.cc b/src/trace_processor/importers/etm/virtual_address_space.cc
index 6e24c7b..9e20214 100644
--- a/src/trace_processor/importers/etm/virtual_address_space.cc
+++ b/src/trace_processor/importers/etm/virtual_address_space.cc
@@ -81,7 +81,7 @@
     if (node.value().end() == *end) {
       slabs.push_back(std::move(node.value()));
     }
-    // The mapping ends at this vertex, no need to split it.
+    // Split needed
     else {
       slabs.push_back(node.value().SplitFront(*end));
       mappings_.insert(std::move(node));
diff --git a/src/trace_processor/importers/etw/etw_module_impl.h b/src/trace_processor/importers/etw/etw_module_impl.h
index 4a27846..0ec15e1 100644
--- a/src/trace_processor/importers/etw/etw_module_impl.h
+++ b/src/trace_processor/importers/etw/etw_module_impl.h
@@ -45,7 +45,7 @@
   void ParseEtwEventData(uint32_t cpu,
                          int64_t ts,
                          const TracePacketData& data) override {
-    util::Status res = parser_.ParseEtwEvent(cpu, ts, data);
+    base::Status res = parser_.ParseEtwEvent(cpu, ts, data);
     if (!res.ok()) {
       PERFETTO_ELOG("%s", res.message().c_str());
     }
diff --git a/src/trace_processor/importers/etw/etw_parser.h b/src/trace_processor/importers/etw/etw_parser.h
index 380c6e8..96306de 100644
--- a/src/trace_processor/importers/etw/etw_parser.h
+++ b/src/trace_processor/importers/etw/etw_parser.h
@@ -30,7 +30,7 @@
  public:
   explicit EtwParser(TraceProcessorContext* context);
 
-  util::Status ParseEtwEvent(uint32_t cpu,
+  base::Status ParseEtwEvent(uint32_t cpu,
                              int64_t ts,
                              const TracePacketData& data);
 
diff --git a/src/trace_processor/importers/ftrace/ftrace_module_impl.h b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
index d6df0f9..64f8fbc 100644
--- a/src/trace_processor/importers/ftrace/ftrace_module_impl.h
+++ b/src/trace_processor/importers/ftrace/ftrace_module_impl.h
@@ -44,7 +44,7 @@
   void ParseFtraceEventData(uint32_t cpu,
                             int64_t ts,
                             const TracePacketData& data) override {
-    util::Status res = parser_.ParseFtraceEvent(cpu, ts, data);
+    base::Status res = parser_.ParseFtraceEvent(cpu, ts, data);
     if (!res.ok()) {
       PERFETTO_ELOG("%s", res.message().c_str());
     }
@@ -53,7 +53,7 @@
   void ParseInlineSchedSwitch(uint32_t cpu,
                               int64_t ts,
                               const InlineSchedSwitch& data) override {
-    util::Status res = parser_.ParseInlineSchedSwitch(cpu, ts, data);
+    base::Status res = parser_.ParseInlineSchedSwitch(cpu, ts, data);
     if (!res.ok()) {
       PERFETTO_ELOG("%s", res.message().c_str());
     }
@@ -62,7 +62,7 @@
   void ParseInlineSchedWaking(uint32_t cpu,
                               int64_t ts,
                               const InlineSchedWaking& data) override {
-    util::Status res = parser_.ParseInlineSchedWaking(cpu, ts, data);
+    base::Status res = parser_.ParseInlineSchedWaking(cpu, ts, data);
     if (!res.ok()) {
       PERFETTO_ELOG("%s", res.message().c_str());
     }
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index a62a374..d5a0528 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -65,6 +65,7 @@
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/softirq_action.h"
 #include "src/trace_processor/types/tcp_state.h"
 #include "src/trace_processor/types/variadic.h"
@@ -452,7 +453,9 @@
           context->storage->InternString("event_type")),
       device_name_id_(context->storage->InternString("device_name")),
       block_io_id_(context->storage->InternString("block_io")),
-      block_io_arg_sector_id_(context->storage->InternString("sector")) {
+      block_io_arg_sector_id_(context->storage->InternString("sector")),
+      cpuhp_action_cpu_id_(context->storage->InternString("action_cpu")),
+      cpuhp_idx_id_(context->storage->InternString("cpuhp_idx")) {
   // Build the lookup table for the strings inside ftrace events (e.g. the
   // name of ftrace event fields and the names of their args).
   for (size_t i = 0; i < GetDescriptorsSize(); i++) {
@@ -1341,6 +1344,17 @@
         ParseBlockIoDone(ts, fld_bytes);
         break;
       }
+      // Intentional fallthrough for Cpuhp multienter/enter, since they both
+      // have same fields and require identical processing.
+      case FtraceEvent::kCpuhpMultiEnterFieldNumber:
+      case FtraceEvent::kCpuhpEnterFieldNumber: {
+        ParseCpuhpEnter(fld.id(), ts, cpu, fld_bytes);
+        break;
+      }
+      case FtraceEvent::kCpuhpExitFieldNumber: {
+        ParseCpuhpExit(ts, fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -1472,9 +1486,10 @@
   StringId event_id = context_->storage->InternString(evt.event_name());
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
   auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
-  RawId id = context_->storage->mutable_ftrace_event_table()
-                 ->Insert({ts, event_id, utid, {}, {}, ucpu})
-                 .id;
+  tables::FtraceEventTable::Id id =
+      context_->storage->mutable_ftrace_event_table()
+          ->Insert({ts, event_id, utid, {}, {}, ucpu})
+          .id;
   auto inserter = context_->args_tracker->AddArgsTo(id);
 
   for (auto it = evt.field(); it; ++it) {
@@ -1514,7 +1529,7 @@
   const auto& message_strings = ftrace_message_strings_[ftrace_id];
   UniqueTid utid = context_->process_tracker->GetOrCreateThread(tid);
   auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
-  RawId id =
+  tables::FtraceEventTable::Id id =
       context_->storage->mutable_ftrace_event_table()
           ->Insert(
               {timestamp, message_strings.message_name_id, utid, {}, {}, ucpu})
@@ -1798,7 +1813,12 @@
     return;
   }
 
-  uint32_t tgid = static_cast<uint32_t>(evt.pid());
+  // dpu_tracing_mark_write can be buggy and can indicate that the swapper
+  // thread is parented to some random process (probably because of IRQs). We
+  // should ignore this as it causes bad cascading effects down the line.
+  //
+  // See b/388130600 for context.
+  uint32_t tgid = pid == 0 ? 0 : static_cast<uint32_t>(evt.pid());
   SystraceParser::GetOrCreate(context_)->ParseKernelTracingMarkWrite(
       timestamp, pid, static_cast<char>(evt.type()), false /*trace_begin*/,
       evt.name(), tgid, evt.value());
@@ -2054,7 +2074,8 @@
   protos::pbzero::DmaHeapStatFtraceEvent::Decoder dma_heap(data);
 
   static constexpr auto kBlueprint = tracks::CounterBlueprint(
-      "dma_heap", tracks::UnknownUnitBlueprint(), tracks::DimensionBlueprints(),
+      "android_dma_heap", tracks::UnknownUnitBlueprint(),
+      tracks::DimensionBlueprints(),
       tracks::StaticNameBlueprint("mem.dma_heap"));
 
   // Push the global counter.
@@ -2063,7 +2084,7 @@
       timestamp, static_cast<double>(dma_heap.total_allocated()), track);
 
   static constexpr auto kChangeBlueprint = tracks::CounterBlueprint(
-      "dma_heap_change", tracks::UnknownUnitBlueprint(),
+      "android_dma_heap_change", tracks::UnknownUnitBlueprint(),
       tracks::Dimensions(tracks::kThreadDimensionBlueprint),
       tracks::StaticNameBlueprint("mem.dma_heap_change"));
 
@@ -3636,7 +3657,12 @@
 
 namespace {
 
-constexpr auto kFuncgraphBlueprint = tracks::SliceBlueprint(
+constexpr auto kThreadFuncgraphBlueprint = tracks::SliceBlueprint(
+    "thread_funcgraph",
+    tracks::DimensionBlueprints(tracks::kThreadDimensionBlueprint),
+    tracks::StaticNameBlueprint("Funcgraph"));
+
+constexpr auto kCpuFuncgraphBlueprint = tracks::SliceBlueprint(
     "cpu_funcgraph",
     tracks::DimensionBlueprints(tracks::kCpuDimensionBlueprint),
     tracks::FnNameBlueprint([](uint32_t cpu) {
@@ -3658,13 +3684,14 @@
   if (pid != 0) {
     // common case: normal thread
     UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-    track = context_->track_tracker->InternThreadTrack(utid);
+    track = context_->track_tracker->InternTrack(kThreadFuncgraphBlueprint,
+                                                 tracks::Dimensions(utid));
   } else {
     // Idle threads (swapper) are implicit, and all share the same thread id
     // 0. Therefore we cannot use a thread-scoped track because many instances
     // of swapper might be running concurrently. Fall back onto global tracks
     // (one per cpu).
-    track = context_->track_tracker->InternTrack(kFuncgraphBlueprint,
+    track = context_->track_tracker->InternTrack(kCpuFuncgraphBlueprint,
                                                  tracks::Dimensions(cpu));
   }
   context_->slice_tracker->Begin(timestamp, track, kNullStringId, name_id);
@@ -3683,10 +3710,11 @@
   if (pid != 0) {
     // common case: normal thread
     UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
-    track = context_->track_tracker->InternThreadTrack(utid);
+    track = context_->track_tracker->InternTrack(kThreadFuncgraphBlueprint,
+                                                 tracks::Dimensions(utid));
   } else {
     // special case: see |ParseFuncgraphEntry|
-    track = context_->track_tracker->InternTrack(kFuncgraphBlueprint,
+    track = context_->track_tracker->InternTrack(kCpuFuncgraphBlueprint,
                                                  tracks::Dimensions(cpu));
   }
   context_->slice_tracker->End(timestamp, track, kNullStringId, name_id);
@@ -3941,4 +3969,60 @@
       });
 }
 
+namespace {
+constexpr auto kCpuHpBlueprint = tracks::SliceBlueprint(
+    "cpu_hotplug",
+    tracks::DimensionBlueprints(tracks::kCpuDimensionBlueprint),
+    tracks::FnNameBlueprint([](uint32_t cpu) {
+      return base::StackString<255>("CPU Hotplug %u", cpu);
+    }));
+}  // namespace
+
+void FtraceParser::ParseCpuhpEnter(uint32_t fld_id,
+                                   int64_t ts,
+                                   uint32_t action_cpu,
+                                   protozero::ConstBytes blob) {
+  uint32_t hp_cpu = UINT32_MAX;
+  int32_t idx = INT32_MAX;
+  switch (fld_id) {
+    case protos::pbzero::FtraceEvent::kCpuhpEnterFieldNumber: {
+      protos::pbzero::CpuhpEnterFtraceEvent::Decoder cpuhp_event(blob);
+      hp_cpu = cpuhp_event.cpu();
+      idx = cpuhp_event.idx();
+      break;
+    }
+    case protos::pbzero::FtraceEvent::kCpuhpMultiEnterFieldNumber: {
+      protos::pbzero::CpuhpMultiEnterFtraceEvent::Decoder cpuhp_event(blob);
+      hp_cpu = cpuhp_event.cpu();
+      idx = cpuhp_event.idx();
+      break;
+    }
+    default:
+      // Only support hotplug_enter and hotplug_multi_enter
+      return;
+  }
+
+  // hp_cpu, the CPU being hotplugged, is stored in track dimension. action_cpu
+  // is the CPU assisting hp_cpu in the hotplug operation. action_cpu could be
+  // the hp_cpu itself or a different CPU, but the distinction is important
+  // since it helps indicate when exactly the hp_cpu is powered off.
+  StringId slice_name_id = context_->storage->InternString(
+      base::StackString<32>("cpuhp(%d)", idx).string_view());
+  TrackId track_id = context_->track_tracker->InternTrack(
+      kCpuHpBlueprint, tracks::Dimensions(hp_cpu));
+  context_->slice_tracker->Begin(
+      ts, track_id, cpu_id_, slice_name_id,
+      [&](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(cpuhp_action_cpu_id_,
+                         Variadic::UnsignedInteger(action_cpu));
+        inserter->AddArg(cpuhp_idx_id_, Variadic::Integer(idx));
+      });
+}
+
+void FtraceParser::ParseCpuhpExit(int64_t ts, protozero::ConstBytes blob) {
+  protos::pbzero::CpuhpExitFtraceEvent::Decoder cpuhp_event(blob);
+  TrackId track_id = context_->track_tracker->InternTrack(
+      kCpuHpBlueprint, tracks::Dimensions(cpuhp_event.cpu()));
+  context_->slice_tracker->End(ts, track_id);
+}
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index 0e05016..08162cc 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -315,6 +315,11 @@
   void ParseParamSetValueCpm(protozero::ConstBytes blob);
   void ParseBlockIoStart(int64_t ts, protozero::ConstBytes blob);
   void ParseBlockIoDone(int64_t ts, protozero::ConstBytes blob);
+  void ParseCpuhpEnter(uint32_t fld_id,
+                       int64_t ts,
+                       uint32_t cpu,
+                       protozero::ConstBytes blob);
+  void ParseCpuhpExit(int64_t ts, protozero::ConstBytes blob);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
@@ -400,6 +405,8 @@
   const StringId device_name_id_;
   const StringId block_io_id_;
   const StringId block_io_arg_sector_id_;
+  const StringId cpuhp_action_cpu_id_;
+  const StringId cpuhp_idx_id_;
 
   std::vector<StringId> syscall_arg_name_ids_;
 
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 833d25a..cb6f033 100644
--- a/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.cc
@@ -235,7 +235,8 @@
     row.ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
 
     // Add an entry to the raw table.
-    RawId id = context_->storage->mutable_ftrace_event_table()->Insert(row).id;
+    tables::FtraceEventTable::Id id =
+        context_->storage->mutable_ftrace_event_table()->Insert(row).id;
 
     using SW = protos::pbzero::SchedWakingFtraceEvent;
     auto inserter = context_->args_tracker->AddArgsTo(id);
@@ -272,9 +273,10 @@
     // Push the raw event - this is done as the raw ftrace event codepath does
     // not insert sched_switch.
     auto ucpu = context_->cpu_tracker->GetOrCreateCpu(cpu);
-    RawId id = context_->storage->mutable_ftrace_event_table()
-                   ->Insert({ts, sched_switch_id_, prev_utid, {}, {}, ucpu})
-                   .id;
+    tables::FtraceEventTable::Id id =
+        context_->storage->mutable_ftrace_event_table()
+            ->Insert({ts, sched_switch_id_, prev_utid, {}, {}, ucpu})
+            .id;
 
     // Note: this ordering is important. The events should be pushed in the same
     // order as the order of fields in the proto; this is used by the raw table
diff --git a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
index 187fb6a..f96bf2b 100644
--- a/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
+++ b/src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc
@@ -24,7 +24,6 @@
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/common/tracks_common.h"
@@ -64,10 +63,13 @@
   row.track_id = track_id;
   row.category = kNullStringId;
   row.name = entry_name_id;
-  row.thread_ts = timestamp;
-  row.thread_dur = active_duration;
-  context_->slice_tracker->ScopedTyped(context_->storage->mutable_slice_table(),
-                                       row);
+  auto slice_id = context_->slice_tracker->Scoped(
+      timestamp, track_id, kNullStringId, entry_name_id, duration);
+  if (slice_id) {
+    auto rr = context_->storage->mutable_slice_table()->FindById(*slice_id);
+    rr->set_thread_ts(timestamp);
+    rr->set_thread_dur(active_duration);
+  }
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/fuchsia/BUILD.gn b/src/trace_processor/importers/fuchsia/BUILD.gn
index d048f86..fc0ee38 100644
--- a/src/trace_processor/importers/fuchsia/BUILD.gn
+++ b/src/trace_processor/importers/fuchsia/BUILD.gn
@@ -85,6 +85,8 @@
     "../../../protozero",
     "../../sorter",
     "../../storage",
+    "../../tables",
+    "../../types",
     "../../util:descriptors",
     "../common",
     "../ftrace:full",
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
index ec29509..5c8e2a9 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
@@ -14,15 +14,18 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
+#include "perfetto/base/status.h"
 
+#include <cstdint>
+#include <cstring>
 #include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
 
-#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "perfetto/trace_processor/trace_blob.h"
-
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
@@ -30,58 +33,37 @@
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/global_args_tracker.h"
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/slice_translation_table.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_sched_event_tracker.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 #include "src/trace_processor/importers/proto/additional_modules.h"
 #include "src/trace_processor/importers/proto/default_modules.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
-#include "src/trace_processor/storage/metadata.h"
+#include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/descriptors.h"
 #include "test/gtest_and_gmock.h"
 
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-#include "protos/perfetto/common/sys_stats_counters.pbzero.h"
-#include "protos/perfetto/config/trace_config.pbzero.h"
-#include "protos/perfetto/trace/android/packages_list.pbzero.h"
-#include "protos/perfetto/trace/chrome/chrome_benchmark_metadata.pbzero.h"
-#include "protos/perfetto/trace/chrome/chrome_trace_event.pbzero.h"
-#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
-#include "protos/perfetto/trace/ftrace/ftrace.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/generic.pbzero.h"
-#include "protos/perfetto/trace/ftrace/power.pbzero.h"
-#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
-#include "protos/perfetto/trace/ftrace/task.pbzero.h"
-#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
-#include "protos/perfetto/trace/sys_stats/sys_stats.pbzero.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
-#include "protos/perfetto/trace/track_event/chrome_thread_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
-#include "protos/perfetto/trace/track_event/log_message.pbzero.h"
-#include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/source_location.pbzero.h"
-#include "protos/perfetto/trace/track_event/task_execution.pbzero.h"
 #include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 using ::testing::_;
 using ::testing::Args;
@@ -197,60 +179,21 @@
               (override));
 };
 
-class MockSliceTracker : public SliceTracker {
- public:
-  explicit MockSliceTracker(TraceProcessorContext* context)
-      : SliceTracker(context) {}
-
-  MOCK_METHOD(std::optional<SliceId>,
-              Begin,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              End,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              Scoped,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               int64_t duration,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              StartSlice,
-              (int64_t timestamp,
-               TrackId track_id,
-               SetArgsCallback args_callback,
-               std::function<SliceId()> inserter),
-              (override));
-};
-
 class FuchsiaTraceParserTest : public ::testing::Test {
  public:
   FuchsiaTraceParserTest() {
-    context_.storage.reset(new TraceStorage());
+    context_.storage = std::make_shared<TraceStorage>();
     storage_ = context_.storage.get();
-    context_.track_tracker.reset(new TrackTracker(&context_));
-    context_.global_args_tracker.reset(
-        new GlobalArgsTracker(context_.storage.get()));
+    context_.track_tracker = std::make_unique<TrackTracker>(&context_);
+    context_.global_args_tracker =
+        std::make_shared<GlobalArgsTracker>(context_.storage.get());
     context_.stack_profile_tracker.reset(new StackProfileTracker(&context_));
-    context_.args_tracker.reset(new ArgsTracker(&context_));
+    context_.args_tracker = std::make_unique<ArgsTracker>(&context_);
     context_.args_translation_table.reset(new ArgsTranslationTable(storage_));
-    context_.metadata_tracker.reset(
-        new MetadataTracker(context_.storage.get()));
-    context_.machine_tracker.reset(new MachineTracker(&context_, 0));
-    context_.cpu_tracker.reset(new CpuTracker(&context_));
+    context_.metadata_tracker =
+        std::make_unique<MetadataTracker>(context_.storage.get());
+    context_.machine_tracker = std::make_unique<MachineTracker>(&context_, 0);
+    context_.cpu_tracker = std::make_unique<CpuTracker>(&context_);
     event_ = new MockEventTracker(&context_);
     context_.event_tracker.reset(event_);
     sched_ = new MockSchedEventTracker(&context_);
@@ -259,17 +202,19 @@
     context_.process_tracker.reset(process_);
     context_.process_track_translation_table.reset(
         new ProcessTrackTranslationTable(storage_));
-    slice_ = new NiceMock<MockSliceTracker>(&context_);
-    context_.slice_tracker.reset(slice_);
-    context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
-    context_.clock_tracker.reset(new ClockTracker(&context_));
+    context_.slice_tracker = std::make_unique<SliceTracker>(&context_);
+    context_.slice_translation_table =
+        std::make_unique<SliceTranslationTable>(storage_);
+    context_.clock_tracker = std::make_unique<ClockTracker>(&context_);
     clock_ = context_.clock_tracker.get();
-    context_.flow_tracker.reset(new FlowTracker(&context_));
-    context_.fuchsia_record_parser.reset(new FuchsiaTraceParser(&context_));
-    context_.proto_trace_parser.reset(new ProtoTraceParserImpl(&context_));
-    context_.sorter.reset(
-        new TraceSorter(&context_, TraceSorter::SortingMode::kFullSort));
-    context_.descriptor_pool_.reset(new DescriptorPool());
+    context_.flow_tracker = std::make_unique<FlowTracker>(&context_);
+    context_.fuchsia_record_parser =
+        std::make_unique<FuchsiaTraceParser>(&context_);
+    context_.proto_trace_parser =
+        std::make_unique<ProtoTraceParserImpl>(&context_);
+    context_.sorter = std::make_shared<TraceSorter>(
+        &context_, TraceSorter::SortingMode::kFullSort);
+    context_.descriptor_pool_ = std::make_unique<DescriptorPool>();
 
     RegisterDefaultModules(&context_);
     RegisterAdditionalModules(&context_);
@@ -285,7 +230,7 @@
 
   void SetUp() override { ResetTraceBuffers(); }
 
-  util::Status Tokenize() {
+  base::Status Tokenize() {
     const size_t num_bytes = trace_bytes_.size() * sizeof(uint64_t);
     std::unique_ptr<uint8_t[]> raw_trace(new uint8_t[num_bytes]);
     memcpy(raw_trace.get(), trace_bytes_.data(), num_bytes);
@@ -305,7 +250,6 @@
   MockEventTracker* event_;
   MockSchedEventTracker* sched_;
   MockProcessTracker* process_;
-  MockSliceTracker* slice_;
   ClockTracker* clock_;
   TraceStorage* storage_;
 };
@@ -475,22 +419,27 @@
   // Only the begin thread time can be imported into the counter table.
   EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(2003000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1005000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(1u))));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, End(1020000, track, unknown_cat, kNullStringId, _))
-      .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
 
   auto status = Tokenize();
   EXPECT_TRUE(status.ok());
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 2u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1005000);
+  EXPECT_EQ(rr_0->track_id(), track);
+
+  auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+  EXPECT_TRUE(rr_1);
+  EXPECT_EQ(rr_1->ts(), 1010000);
+  EXPECT_EQ(rr_1->track_id(), track);
+  EXPECT_EQ(rr_1->dur(), 10000);
+  EXPECT_EQ(rr_1->category(), unknown_cat);
 }
 
 TEST_F(FuchsiaTraceParserTest, SchedulerEvents) {
@@ -620,5 +569,4 @@
 }
 
 }  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
index ea6197d..8632648 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.cc
@@ -83,7 +83,7 @@
 
 FuchsiaTraceTokenizer::~FuchsiaTraceTokenizer() = default;
 
-util::Status FuchsiaTraceTokenizer::Parse(TraceBlobView blob) {
+base::Status FuchsiaTraceTokenizer::Parse(TraceBlobView blob) {
   size_t size = blob.size();
 
   // The relevant internal state is |leftover_bytes_|. Each call to Parse should
@@ -111,7 +111,7 @@
     // record, so just add the new bytes to |leftover_bytes_| and return.
     leftover_bytes_.insert(leftover_bytes_.end(), blob.data() + byte_offset,
                            blob.data() + size);
-    return util::OkStatus();
+    return base::OkStatus();
   }
   if (!leftover_bytes_.empty()) {
     // There is a record starting from leftover bytes.
@@ -155,7 +155,7 @@
       // have to leftover_bytes_ and wait for more.
       leftover_bytes_.insert(leftover_bytes_.end(), blob.data() + byte_offset,
                              blob.data() + byte_offset + size);
-      return util::OkStatus();
+      return base::OkStatus();
     }
   }
 
@@ -172,7 +172,7 @@
         fuchsia_trace_utils::ReadField<uint32_t>(header, 4, 15) *
         sizeof(uint64_t);
     if (record_len_bytes == 0)
-      return util::ErrStatus("Unexpected record of size 0");
+      return base::ErrStatus("Unexpected record of size 0");
 
     if (record_offset + record_len_bytes > size)
       break;
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
index 06038b7..409644f 100644
--- a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
@@ -280,6 +280,8 @@
           std::string key(attrs[i]);
           if (key == "addr") {
             new_frame.ptr->addr = strtoll(attrs[i + 1], nullptr, 16);
+          } else if (key == "name") {
+            new_frame.ptr->name = std::string(attrs[i + 1]);
           }
         }
         current_new_frame_ = new_frame.id;
diff --git a/src/trace_processor/importers/instruments/row.h b/src/trace_processor/importers/instruments/row.h
index 531a36d..55a021c 100644
--- a/src/trace_processor/importers/instruments/row.h
+++ b/src/trace_processor/importers/instruments/row.h
@@ -41,6 +41,7 @@
 
 struct Frame {
   long long addr = 0;
+  std::string name;
   BinaryId binary = kNullId;
 };
 
diff --git a/src/trace_processor/importers/instruments/row_parser.cc b/src/trace_processor/importers/instruments/row_parser.cc
index 39d7a92..1cfa13e 100644
--- a/src/trace_processor/importers/instruments/row_parser.cc
+++ b/src/trace_processor/importers/instruments/row_parser.cc
@@ -64,47 +64,47 @@
     Frame* frame = data_.GetFrame(*it);
     Binary* binary = data_.GetBinary(frame->binary);
 
-    uint64_t rel_pc = static_cast<uint64_t>(frame->addr);
+    uint64_t pc = static_cast<uint64_t>(frame->addr);
     if (frame->binary) {
-      rel_pc -= static_cast<uint64_t>(binary->load_addr);
+      pc -= static_cast<uint64_t>(binary->load_addr);
     }
 
     // For non-leaf functions, the pc will be after the end of the call. Adjust
     // it to be within the call instruction.
-    if (rel_pc != 0 && it != leaf) {
-      --rel_pc;
+    if (pc != 0 && it != leaf) {
+      --pc;
     }
 
-    auto frame_inserted = frame_to_frame_id_.Insert(*it, FrameId{0});
-    if (frame_inserted.second) {
-      auto mapping_inserted = binary_to_mapping_.Insert(frame->binary, nullptr);
-      if (mapping_inserted.second) {
-        if (binary == nullptr) {
-          *mapping_inserted.first = GetDummyMapping(upid);
-        } else {
+    VirtualMemoryMapping* mapping = nullptr;
+    mapping = context_->mapping_tracker->FindUserMappingForAddress(upid, pc);
+    if (!mapping) {
+      if (binary == nullptr) {
+        mapping = GetDummyMapping(upid);
+      } else {
+        auto mapping_inserted =
+            binary_to_mapping_.Insert(frame->binary, nullptr);
+        if (mapping_inserted.second) {
           BuildId build_id = binary->uuid;
           *mapping_inserted.first =
               &context_->mapping_tracker->CreateUserMemoryMapping(
                   upid, {AddressRange(static_cast<uint64_t>(binary->load_addr),
                                       static_cast<uint64_t>(binary->max_addr)),
-                         0, 0, 0, binary->path, build_id});
+                         static_cast<uint64_t>(binary->load_addr), 0, 0,
+                         binary->path, build_id});
         }
+        mapping = *mapping_inserted.first;
       }
-      VirtualMemoryMapping* mapping = *mapping_inserted.first;
-
-      // Intern the frame with no function name -- the symbolizer will annotate
-      // frames later.
-      *frame_inserted.first =
-          mapping->InternFrame(rel_pc, base::StringView(""));
     }
-    FrameId frame_id = *frame_inserted.first;
+
+    FrameId frame_id = mapping->InternFrame(mapping->ToRelativePc(pc),
+                                            base::StringView(frame->name));
 
     parent = stack_profile_tracker.InternCallsite(parent, frame_id, depth);
     depth++;
   }
 
   context_->storage->mutable_instruments_sample_table()->Insert(
-      {ts, utid, row.core_id, parent});
+      {ts, utid, parent, row.core_id});
 }
 
 DummyMemoryMapping* RowParser::GetDummyMapping(UniquePid upid) {
diff --git a/src/trace_processor/importers/instruments/row_parser.h b/src/trace_processor/importers/instruments/row_parser.h
index ead208c..6c15520 100644
--- a/src/trace_processor/importers/instruments/row_parser.h
+++ b/src/trace_processor/importers/instruments/row_parser.h
@@ -41,10 +41,8 @@
   TraceProcessorContext* context_;
   RowDataTracker& data_;
 
-  // Cache FrameId and binary mappings by instruments frame and binary
-  // pointers, respectively. These are already de-duplicated in the
-  // instruments XML parsing.
-  base::FlatHashMap<BacktraceFrameId, FrameId> frame_to_frame_id_;
+  // Cache binary mappings by instruments binary pointers. These are already
+  // de-duplicated in the instruments XML parsing.
   base::FlatHashMap<BinaryId, VirtualMemoryMapping*> binary_to_mapping_;
   base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
 };
diff --git a/src/trace_processor/importers/json/json_trace_parser_impl.cc b/src/trace_processor/importers/json/json_trace_parser_impl.cc
index 77953af..7b30623 100644
--- a/src/trace_processor/importers/json/json_trace_parser_impl.cc
+++ b/src/trace_processor/importers/json/json_trace_parser_impl.cc
@@ -151,27 +151,20 @@
   // Only used for 'B', 'E', and 'X' events so wrap in lambda so it gets
   // ignored in other cases. This lambda is only safe to call within the
   // scope of this function due to the capture by reference.
-  auto make_slice_row = [&](TrackId track_id) {
-    tables::SliceTable::Row row;
-    row.ts = timestamp;
-    row.track_id = track_id;
-    row.category = cat_id;
-    row.name =
-        name_id == kNullStringId ? storage->InternString("[No name]") : name_id;
-    row.thread_ts = json::CoerceToTs(value["tts"]);
-    // tdur will only exist on 'X' events.
-    row.thread_dur = json::CoerceToTs(value["tdur"]);
-    // JSON traces don't report these counters as part of slices.
-    row.thread_instruction_count = std::nullopt;
-    row.thread_instruction_delta = std::nullopt;
-    return row;
-  };
-
+  StringId slice_name_id =
+      name_id == kNullStringId ? storage->InternString("[No name]") : name_id;
   switch (phase) {
     case 'B': {  // TRACE_EVENT_BEGIN.
       TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-      slice_tracker->BeginTyped(storage->mutable_slice_table(),
-                                make_slice_row(track_id), args_inserter);
+      auto slice_id = slice_tracker->Begin(timestamp, track_id, cat_id,
+                                           slice_name_id, args_inserter);
+      if (slice_id) {
+        if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+          auto rr =
+              context_->storage->mutable_slice_table()->FindById(*slice_id);
+          rr->set_thread_ts(*thread_ts);
+        }
+      }
       MaybeAddFlow(track_id, value);
       break;
     }
@@ -220,8 +213,8 @@
       }
 
       if (phase == 'b') {
-        slice_tracker->BeginTyped(storage->mutable_slice_table(),
-                                  make_slice_row(track_id), args_inserter);
+        slice_tracker->Begin(timestamp, track_id, cat_id, slice_name_id,
+                             args_inserter);
         MaybeAddFlow(track_id, value);
       } else if (phase == 'e') {
         slice_tracker->End(timestamp, track_id, cat_id, name_id, args_inserter);
@@ -239,10 +232,17 @@
       if (!opt_dur.has_value())
         return;
       TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
-      auto row = make_slice_row(track_id);
-      row.dur = opt_dur.value();
-      slice_tracker->ScopedTyped(storage->mutable_slice_table(), std::move(row),
-                                 args_inserter);
+      auto slice_id = slice_tracker->Scoped(
+          timestamp, track_id, cat_id, slice_name_id, *opt_dur, args_inserter);
+      if (slice_id) {
+        auto rr = context_->storage->mutable_slice_table()->FindById(*slice_id);
+        if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+          rr->set_thread_ts(*thread_ts);
+        }
+        if (auto thread_dur = json::CoerceToTs(value["tdur"]); thread_dur) {
+          rr->set_thread_dur(*thread_dur);
+        }
+      }
       MaybeAddFlow(track_id, value);
       break;
     }
@@ -318,14 +318,15 @@
           break;
         }
         track_id = context_->track_tracker->InternThreadTrack(utid);
-        auto row = make_slice_row(track_id);
-        row.dur = 0;
-        if (row.thread_ts) {
-          // Only set thread_dur to zero if we have a thread_ts.
-          row.thread_dur = 0;
+        auto slice_id = slice_tracker->Scoped(timestamp, track_id, cat_id,
+                                              slice_name_id, 0, args_inserter);
+        if (slice_id) {
+          if (auto thread_ts = json::CoerceToTs(value["tts"]); thread_ts) {
+            auto rr =
+                context_->storage->mutable_slice_table()->FindById(*slice_id);
+            rr->set_thread_ts(*thread_ts);
+          }
         }
-        slice_tracker->ScopedTyped(storage->mutable_slice_table(),
-                                   std::move(row), args_inserter);
         break;
       } else {
         context_->storage->IncrementStats(stats::json_parser_failure);
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 8ec36af..f4f5e47 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -30,6 +30,7 @@
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/sorter/trace_sorter.h"  // IWYU pragma: keep
 #include "src/trace_processor/storage/stats.h"
@@ -576,7 +577,27 @@
         continue;
       }
     }
-    context_->sorter->PushJsonValue(ts, unparsed.ToStdString(), opt_dur);
+    JsonEvent::Type type;
+    if (opt_raw_ph && opt_raw_ph->size() == 1) {
+      switch ((*opt_raw_ph)[0]) {
+        case 'B':
+          type = JsonEvent::Begin();
+          break;
+        case 'E':
+          type = JsonEvent::End();
+          break;
+        case 'X':
+          if (opt_dur) {
+            type = JsonEvent::Scoped{*opt_dur};
+          } else {
+            type = JsonEvent::Other();
+          }
+          break;
+      }
+    } else {
+      type = JsonEvent::Other();
+    }
+    context_->sorter->PushJsonValue(ts, unparsed.ToStdString(), type);
   }
   return SetOutAndReturn(next, out);
 }
diff --git a/src/trace_processor/importers/perf/features.cc b/src/trace_processor/importers/perf/features.cc
index 877f2fd..be65da57 100644
--- a/src/trace_processor/importers/perf/features.cc
+++ b/src/trace_processor/importers/perf/features.cc
@@ -107,31 +107,31 @@
   return true;
 }
 
-util::Status ParseEventTypeInfo(std::string value, SimpleperfMetaInfo& out) {
+base::Status ParseEventTypeInfo(std::string value, SimpleperfMetaInfo& out) {
   for (const auto& line : base::SplitString(value, "\n")) {
     auto tokens = base::SplitString(line, ",");
     if (tokens.size() != 3) {
-      return util::ErrStatus("Invalid event_type_info: '%s'", line.c_str());
+      return base::ErrStatus("Invalid event_type_info: '%s'", line.c_str());
     }
 
     auto type = base::StringToUInt32(tokens[1]);
     if (!type) {
-      return util::ErrStatus("Could not parse type in event_type_info: '%s'",
+      return base::ErrStatus("Could not parse type in event_type_info: '%s'",
                              tokens[1].c_str());
     }
     auto config = base::StringToUInt64(tokens[2]);
     if (!config) {
-      return util::ErrStatus("Could not parse config in event_type_info: '%s'",
+      return base::ErrStatus("Could not parse config in event_type_info: '%s'",
                              tokens[2].c_str());
     }
 
     out.event_type_info.Insert({*type, *config}, std::move(tokens[0]));
   }
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status ParseSimpleperfMetaInfoEntry(
+base::Status ParseSimpleperfMetaInfoEntry(
     std::pair<std::string, std::string> entry,
     SimpleperfMetaInfo& out) {
   static constexpr char kEventTypeInfoKey[] = "event_type_info";
@@ -142,14 +142,14 @@
   PERFETTO_CHECK(
       out.entries.Insert(std::move(entry.first), std::move(entry.second))
           .second);
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 }  // namespace
 
 // static
-util::Status BuildId::Parse(TraceBlobView bytes,
-                            std::function<util::Status(BuildId)> cb) {
+base::Status BuildId::Parse(TraceBlobView bytes,
+                            std::function<base::Status(BuildId)> cb) {
   Reader reader(std::move(bytes));
   while (reader.size_left() != 0) {
     perf_event_header header;
@@ -175,27 +175,27 @@
 
     RETURN_IF_ERROR(cb(std::move(build_id)));
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 // static
-util::Status SimpleperfMetaInfo::Parse(const TraceBlobView& bytes,
+base::Status SimpleperfMetaInfo::Parse(const TraceBlobView& bytes,
                                        SimpleperfMetaInfo& out) {
   auto* it_end = reinterpret_cast<const char*>(bytes.data() + bytes.size());
   for (auto* it = reinterpret_cast<const char*>(bytes.data()); it != it_end;) {
     auto end = std::find(it, it_end, '\0');
     if (end == it_end) {
-      return util::ErrStatus("Failed to read key from Simpleperf MetaInfo");
+      return base::ErrStatus("Failed to read key from Simpleperf MetaInfo");
     }
     std::string key(it, end);
     it = end;
     ++it;
     if (it == it_end) {
-      return util::ErrStatus("Missing value in Simpleperf MetaInfo");
+      return base::ErrStatus("Missing value in Simpleperf MetaInfo");
     }
     end = std::find(it, it_end, '\0');
     if (end == it_end) {
-      return util::ErrStatus("Failed to read value from Simpleperf MetaInfo");
+      return base::ErrStatus("Failed to read value from Simpleperf MetaInfo");
     }
     std::string value(it, end);
     it = end;
@@ -204,18 +204,18 @@
     RETURN_IF_ERROR(ParseSimpleperfMetaInfoEntry(
         std::make_pair(std::move(key), std::move(value)), out));
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 // static
-util::Status EventDescription::Parse(
+base::Status EventDescription::Parse(
     TraceBlobView bytes,
-    std::function<util::Status(EventDescription)> cb) {
+    std::function<base::Status(EventDescription)> cb) {
   Reader reader(std::move(bytes));
   uint32_t nr;
   uint32_t attr_size;
   if (!reader.Read(nr) || !reader.Read(attr_size)) {
-    return util::ErrStatus("Failed to parse header for PERF_EVENT_DESC");
+    return base::ErrStatus("Failed to parse header for PERF_EVENT_DESC");
   }
 
   for (; nr != 0; --nr) {
@@ -223,21 +223,21 @@
     uint32_t nr_ids;
     if (!reader.ReadPerfEventAttr(desc.attr, attr_size) ||
         !reader.Read(nr_ids) || !ParseString(reader, desc.event_string)) {
-      return util::ErrStatus("Failed to parse record for PERF_EVENT_DESC");
+      return base::ErrStatus("Failed to parse record for PERF_EVENT_DESC");
     }
 
     desc.ids.resize(nr_ids);
     for (uint64_t& id : desc.ids) {
       if (!reader.Read(id)) {
-        return util::ErrStatus("Failed to parse ids for PERF_EVENT_DESC");
+        return base::ErrStatus("Failed to parse ids for PERF_EVENT_DESC");
       }
     }
     RETURN_IF_ERROR(cb(std::move(desc)));
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status ParseSimpleperfFile2(TraceBlobView bytes,
+base::Status ParseSimpleperfFile2(TraceBlobView bytes,
                                   std::function<void(TraceBlobView)> cb) {
   Reader reader(std::move(bytes));
   while (reader.size_left() != 0) {
@@ -252,15 +252,15 @@
     }
     cb(std::move(payload));
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 // static
-util::Status HeaderGroupDesc::Parse(TraceBlobView bytes, HeaderGroupDesc& out) {
+base::Status HeaderGroupDesc::Parse(TraceBlobView bytes, HeaderGroupDesc& out) {
   Reader reader(std::move(bytes));
   uint32_t nr;
   if (!reader.Read(nr)) {
-    return util::ErrStatus("Failed to parse header for HEADER_GROUP_DESC");
+    return base::ErrStatus("Failed to parse header for HEADER_GROUP_DESC");
   }
 
   HeaderGroupDesc group_desc;
@@ -268,7 +268,7 @@
   for (auto& e : group_desc.entries) {
     if (!ParseString(reader, e.string) || !reader.Read(e.leader_idx) ||
         !reader.Read(e.nr_members)) {
-      return util::ErrStatus("Failed to parse HEADER_GROUP_DESC entry");
+      return base::ErrStatus("Failed to parse HEADER_GROUP_DESC entry");
     }
   }
   out = std::move(group_desc);
@@ -279,7 +279,7 @@
   Reader reader(std::move(bytes));
   uint32_t nr;
   if (!reader.Read(nr)) {
-    return util::ErrStatus("Failed to parse nr for CMDLINE");
+    return base::ErrStatus("Failed to parse nr for CMDLINE");
   }
 
   std::vector<std::string> args;
diff --git a/src/trace_processor/importers/perf/features.h b/src/trace_processor/importers/perf/features.h
index e028957..12aeaea 100644
--- a/src/trace_processor/importers/perf/features.h
+++ b/src/trace_processor/importers/perf/features.h
@@ -74,15 +74,15 @@
 };
 
 struct BuildId {
-  static util::Status Parse(TraceBlobView,
-                            std::function<util::Status(BuildId)> cb);
+  static base::Status Parse(TraceBlobView,
+                            std::function<base::Status(BuildId)> cb);
   int32_t pid;
   std::string build_id;
   std::string filename;
 };
 
 struct HeaderGroupDesc {
-  static util::Status Parse(TraceBlobView, HeaderGroupDesc& out);
+  static base::Status Parse(TraceBlobView, HeaderGroupDesc& out);
   struct Entry {
     std::string string;
     uint32_t leader_idx;
@@ -92,15 +92,15 @@
 };
 
 struct EventDescription {
-  static util::Status Parse(TraceBlobView,
-                            std::function<util::Status(EventDescription)> cb);
+  static base::Status Parse(TraceBlobView,
+                            std::function<base::Status(EventDescription)> cb);
   perf_event_attr attr;
   std::string event_string;
   std::vector<uint64_t> ids;
 };
 
 struct SimpleperfMetaInfo {
-  static util::Status Parse(const TraceBlobView&, SimpleperfMetaInfo& out);
+  static base::Status Parse(const TraceBlobView&, SimpleperfMetaInfo& out);
   base::FlatHashMap<std::string, std::string> entries;
   struct EventTypeAndConfig {
     uint32_t type;
@@ -122,7 +122,7 @@
       event_type_info;
 };
 
-util::Status ParseSimpleperfFile2(TraceBlobView,
+base::Status ParseSimpleperfFile2(TraceBlobView,
                                   std::function<void(TraceBlobView)> cb);
 
 base::StatusOr<std::vector<std::string>> ParseCmdline(TraceBlobView blob);
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index aa61056..a5b9181 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -28,8 +28,6 @@
     "chrome_system_probes_parser.h",
     "default_modules.cc",
     "default_modules.h",
-    "jit_tracker.cc",
-    "jit_tracker.h",
     "memory_tracker_snapshot_module.cc",
     "memory_tracker_snapshot_module.h",
     "memory_tracker_snapshot_parser.cc",
@@ -46,8 +44,6 @@
     "packet_sequence_state_generation.cc",
     "perf_sample_tracker.cc",
     "perf_sample_tracker.h",
-    "profile_module.cc",
-    "profile_module.h",
     "profile_packet_sequence_state.cc",
     "profile_packet_sequence_state.h",
     "profile_packet_utils.cc",
@@ -72,6 +68,9 @@
   ]
   public_deps = [ ":proto_importer_module" ]
   deps = [
+    ":gen_cc_android_track_event_descriptor",
+    ":gen_cc_chrome_track_event_descriptor",
+    ":gen_cc_track_event_descriptor",
     "../../../../gn:default_deps",
     "../../../../include/perfetto/trace_processor:trace_processor",
     "../../../../protos/perfetto/common:zero",
@@ -136,6 +135,8 @@
     "heap_graph_module.h",
     "heap_graph_tracker.cc",
     "heap_graph_tracker.h",
+    "jit_tracker.cc",
+    "jit_tracker.h",
     "metadata_module.cc",
     "metadata_module.h",
     "pigweed_detokenizer.cc",
@@ -144,6 +145,8 @@
     "pixel_modem_module.h",
     "pixel_modem_parser.cc",
     "pixel_modem_parser.h",
+    "profile_module.cc",
+    "profile_module.h",
     "statsd_module.cc",
     "statsd_module.h",
     "string_encoding_utils.cc",
@@ -164,7 +167,6 @@
     "vulkan_memory_tracker.h",
   ]
   deps = [
-    ":gen_cc_config_descriptor",
     ":gen_cc_statsd_atoms_descriptor",
     ":gen_cc_trace_descriptor",
     ":minimal",
@@ -193,6 +195,7 @@
     "../../storage",
     "../../tables",
     "../../types",
+    "../../util:build_id",
     "../../util:descriptors",
     "../../util:profiler_util",
     "../../util:proto_profiler",
@@ -263,11 +266,6 @@
       "../../../../protos/perfetto/trace/android:android_track_event_descriptor"
 }
 
-perfetto_cc_proto_descriptor("gen_cc_config_descriptor") {
-  descriptor_name = "config.descriptor"
-  descriptor_target = "../../../../protos/perfetto/config:descriptor"
-}
-
 source_set("unittests") {
   testonly = true
   sources = [
@@ -284,6 +282,7 @@
   ]
   deps = [
     ":full",
+    ":gen_cc_trace_descriptor",
     ":minimal",
     ":packet_sequence_state_generation_hdr",
     "../../../../gn:default_deps",
diff --git a/src/trace_processor/importers/proto/additional_modules.cc b/src/trace_processor/importers/proto/additional_modules.cc
index 0805476..296de58 100644
--- a/src/trace_processor/importers/proto/additional_modules.cc
+++ b/src/trace_processor/importers/proto/additional_modules.cc
@@ -15,26 +15,35 @@
  */
 
 #include "src/trace_processor/importers/proto/additional_modules.h"
+
+#include <memory>
+
 #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"
+#include "src/trace_processor/importers/proto/content_analyzer.h"
 #include "src/trace_processor/importers/proto/graphics_event_module.h"
 #include "src/trace_processor/importers/proto/heap_graph_module.h"
 #include "src/trace_processor/importers/proto/metadata_module.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/network_trace_module.h"
 #include "src/trace_processor/importers/proto/pixel_modem_module.h"
+#include "src/trace_processor/importers/proto/profile_module.h"
 #include "src/trace_processor/importers/proto/statsd_module.h"
 #include "src/trace_processor/importers/proto/system_probes_module.h"
+#include "src/trace_processor/importers/proto/trace.descriptor.h"
 #include "src/trace_processor/importers/proto/translation_table_module.h"
 #include "src/trace_processor/importers/proto/v8_module.h"
 #include "src/trace_processor/importers/proto/winscope/winscope_module.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 void RegisterAdditionalModules(TraceProcessorContext* context) {
+  // Content analyzer and metadata module both depend on this.
+  context->descriptor_pool_->AddFromFileDescriptorSet(kTraceDescriptor.data(),
+                                                      kTraceDescriptor.size());
+
   context->modules.emplace_back(new AndroidProbesModule(context));
   context->modules.emplace_back(new NetworkTraceModule(context));
   context->modules.emplace_back(new GraphicsEventModule(context));
@@ -47,6 +56,7 @@
   context->modules.emplace_back(new V8Module(context));
   context->modules.emplace_back(new WinscopeModule(context));
   context->modules.emplace_back(new PixelModemModule(context));
+  context->modules.emplace_back(new ProfileModule(context));
 
   // 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.
@@ -60,7 +70,10 @@
     context->multi_machine_trace_manager->EnableAdditionalModules(
         RegisterAdditionalModules);
   }
+
+  if (context->config.analyze_trace_proto_content) {
+    context->content_analyzer = std::make_unique<ProtoContentAnalyzer>(context);
+  }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/android_probes_module.cc b/src/trace_processor/importers/proto/android_probes_module.cc
index 9976e70..7163ab5 100644
--- a/src/trace_processor/importers/proto/android_probes_module.cc
+++ b/src/trace_processor/importers/proto/android_probes_module.cc
@@ -51,6 +51,7 @@
 #include "protos/perfetto/trace/power/android_entity_state_residency.pbzero.h"
 #include "protos/perfetto/trace/power/power_rails.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "protos/perfetto/trace/android/bluetooth_trace.pbzero.h"
 
 namespace perfetto::trace_processor {
 namespace {
@@ -115,6 +116,7 @@
                    context);
   RegisterForField(TracePacket::kInitialDisplayStateFieldNumber, context);
   RegisterForField(TracePacket::kAndroidSystemPropertyFieldNumber, context);
+  RegisterForField(TracePacket::kBluetoothTraceEventFieldNumber, context);
 }
 
 ModuleResult AndroidProbesModule::TokenizePacket(
@@ -260,6 +262,9 @@
     case TracePacket::kAndroidSystemPropertyFieldNumber:
       parser_.ParseAndroidSystemProperty(ts, decoder.android_system_property());
       return;
+    case TracePacket::kBluetoothTraceEventFieldNumber:
+      parser_.ParseBtTraceEvent(ts, decoder.bluetooth_trace_event());
+      return;
   }
 }
 
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 7075be5..c6bbb3c 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -56,6 +56,7 @@
 #include "protos/perfetto/trace/power/android_entity_state_residency.pbzero.h"
 #include "protos/perfetto/trace/power/battery_counters.pbzero.h"
 #include "protos/perfetto/trace/power/power_rails.pbzero.h"
+#include "protos/perfetto/trace/android/bluetooth_trace.pbzero.h"
 
 namespace perfetto::trace_processor {
 
@@ -67,7 +68,16 @@
       energy_consumer_id_(
           context_->storage->InternString("energy_consumer_id")),
       consumer_type_id_(context_->storage->InternString("consumer_type")),
-      ordinal_id_(context_->storage->InternString("ordinal")) {}
+      ordinal_id_(context_->storage->InternString("ordinal")),
+      bt_trace_event_id_(context_->storage->InternString("BluetoothTraceEvent")),
+      bt_packet_type_id_(context_->storage->InternString("TracePacketType")),
+      bt_count_id_(context_->storage->InternString("Count")),
+      bt_length_id_(context_->storage->InternString("Length")),
+      bt_duration_id_(context_->storage->InternString("Duration")),
+      bt_op_code_id_(context_->storage->InternString("Op Code")),
+      bt_event_code_id_(context_->storage->InternString("Event Code")),
+      bt_subevent_code_id_(context_->storage->InternString("Subevent Code")),
+      bt_handle_id_(context_->storage->InternString("Handle")) {}
 
 void AndroidProbesParser::ParseBatteryCounters(int64_t ts, ConstBytes blob) {
   protos::pbzero::BatteryCounters::Decoder evt(blob);
@@ -491,4 +501,65 @@
   }
 }
 
+void AndroidProbesParser::ParseBtTraceEvent(int64_t ts,
+                                            ConstBytes blob) {
+  protos::pbzero::BluetoothTraceEvent::Decoder evt(blob);
+
+  static constexpr auto kBluetoothTraceEventBlueprint =
+    tracks::SliceBlueprint("bluetooth_trace_event",
+                           tracks::DimensionBlueprints(),
+                           tracks::StaticNameBlueprint("BluetoothTraceEvent"));
+
+  TrackId track_id = context_->track_tracker->InternTrack(
+      kBluetoothTraceEventBlueprint);
+
+  context_->slice_tracker->Scoped(ts, track_id, kNullStringId,
+    bt_trace_event_id_, 0, [&evt, this](ArgsTracker::BoundInserter* inserter) {
+      if (evt.has_packet_type()) {
+        StringId packet_type_str = context_->storage->InternString(
+          protos::pbzero::BluetoothTracePacketType_Name(
+            static_cast<::perfetto::protos::pbzero::BluetoothTracePacketType>(
+              evt.packet_type())));
+        inserter->AddArg(
+          bt_packet_type_id_,
+          Variadic::String(packet_type_str));
+      }
+      if (evt.has_count()) {
+        inserter->AddArg(
+          bt_count_id_,
+          Variadic::UnsignedInteger(evt.count()));
+      }
+      if (evt.has_length()) {
+        inserter->AddArg(
+          bt_length_id_,
+          Variadic::UnsignedInteger(evt.length()));
+      }
+      if (evt.has_duration()) {
+        inserter->AddArg(
+          bt_duration_id_,
+          Variadic::UnsignedInteger(evt.duration()));
+      }
+      if (evt.has_op_code()) {
+        inserter->AddArg(
+          bt_op_code_id_,
+          Variadic::UnsignedInteger(evt.op_code()));
+      }
+      if (evt.has_event_code()) {
+        inserter->AddArg(
+          bt_event_code_id_,
+          Variadic::UnsignedInteger(evt.event_code()));
+      }
+      if (evt.has_subevent_code()) {
+        inserter->AddArg(
+          bt_subevent_code_id_,
+          Variadic::UnsignedInteger(evt.subevent_code()));
+      }
+      if (evt.has_connection_handle()) {
+        inserter->AddArg(
+          bt_handle_id_,
+          Variadic::UnsignedInteger(evt.connection_handle()));
+      }
+    });
+}
+
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/android_probes_parser.h b/src/trace_processor/importers/proto/android_probes_parser.h
index 8a8a04a..613ac77 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.h
+++ b/src/trace_processor/importers/proto/android_probes_parser.h
@@ -43,6 +43,7 @@
   void ParseInitialDisplayState(int64_t ts, ConstBytes);
   void ParseAndroidSystemProperty(int64_t ts, ConstBytes);
   void ParseAndroidGameIntervention(ConstBytes);
+  void ParseBtTraceEvent(int64_t ts, ConstBytes);
 
  private:
   TraceProcessorContext* const context_;
@@ -53,6 +54,15 @@
   const StringId energy_consumer_id_;
   const StringId consumer_type_id_;
   const StringId ordinal_id_;
+  const StringId bt_trace_event_id_;
+  const StringId bt_packet_type_id_;
+  const StringId bt_count_id_;
+  const StringId bt_length_id_;
+  const StringId bt_duration_id_;
+  const StringId bt_op_code_id_;
+  const StringId bt_event_code_id_;
+  const StringId bt_subevent_code_id_;
+  const StringId bt_handle_id_;
 };
 }  // namespace perfetto::trace_processor
 
diff --git a/src/trace_processor/importers/proto/args_parser.cc b/src/trace_processor/importers/proto/args_parser.cc
index 2aeb785..9692d2c 100644
--- a/src/trace_processor/importers/proto/args_parser.cc
+++ b/src/trace_processor/importers/proto/args_parser.cc
@@ -67,10 +67,10 @@
                    Variadic::Real(value));
 }
 
-void ArgsParser::AddPointer(const Key& key, const void* value) {
+void ArgsParser::AddPointer(const Key& key, uint64_t value) {
   inserter_.AddArg(storage_.InternString(base::StringView(key.flat_key)),
                    storage_.InternString(base::StringView(key.key)),
-                   Variadic::Pointer(reinterpret_cast<uintptr_t>(value)));
+                   Variadic::Pointer(reinterpret_cast<uint64_t>(value)));
 }
 
 void ArgsParser::AddBoolean(const Key& key, bool value) {
diff --git a/src/trace_processor/importers/proto/args_parser.h b/src/trace_processor/importers/proto/args_parser.h
index 7cd1578..c557891 100644
--- a/src/trace_processor/importers/proto/args_parser.h
+++ b/src/trace_processor/importers/proto/args_parser.h
@@ -41,7 +41,7 @@
   void AddString(const Key&, const protozero::ConstChars&) override;
   void AddString(const Key&, const std::string&) override;
   void AddDouble(const Key&, double) override;
-  void AddPointer(const Key&, const void*) override;
+  void AddPointer(const Key&, uint64_t) override;
   void AddBoolean(const Key&, bool) override;
   void AddBytes(const Key&, const protozero::ConstBytes&) override;
   bool AddJson(const Key&, const protozero::ConstChars&) override;
diff --git a/src/trace_processor/importers/proto/content_analyzer.cc b/src/trace_processor/importers/proto/content_analyzer.cc
index 0123c44..025f138 100644
--- a/src/trace_processor/importers/proto/content_analyzer.cc
+++ b/src/trace_processor/importers/proto/content_analyzer.cc
@@ -16,29 +16,26 @@
 
 #include "src/trace_processor/importers/proto/content_analyzer.h"
 
-#include "perfetto/ext/base/string_utils.h"
+#include <cstdint>
+#include <optional>
+#include <string>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/content_analyzer.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/trace_proto_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/util/proto_profiler.h"
 
-#include "src/trace_processor/importers/proto/trace.descriptor.h"
-
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 ProtoContentAnalyzer::ProtoContentAnalyzer(TraceProcessorContext* context)
     : context_(context),
-      pool_([]() {
-        DescriptorPool pool;
-        base::Status status = pool.AddFromFileDescriptorSet(
-            kTraceDescriptor.data(), kTraceDescriptor.size());
-        if (!status.ok()) {
-          PERFETTO_ELOG("Could not add TracePacket proto descriptor %s",
-                        status.c_message());
-        }
-        return pool;
-      }()),
-      computer_(&pool_, ".perfetto.protos.TracePacket") {}
+      computer_(context_->descriptor_pool_.get(),
+                ".perfetto.protos.TracePacket") {}
 
 ProtoContentAnalyzer::~ProtoContentAnalyzer() = default;
 
@@ -135,5 +132,4 @@
   aggregated_samples_.Clear();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/content_analyzer.h b/src/trace_processor/importers/proto/content_analyzer.h
index 84fd674..eabb858 100644
--- a/src/trace_processor/importers/proto/content_analyzer.h
+++ b/src/trace_processor/importers/proto/content_analyzer.h
@@ -17,18 +17,17 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
 
+#include <cstddef>
 #include <utility>
-#include <vector>
 
 #include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
-#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/proto_profiler.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Interface for a module that processes track event information.
 class ProtoContentAnalyzer : public PacketAnalyzer {
@@ -59,22 +58,19 @@
   using AnnotatedSamplesMap = base::
       FlatHashMap<SampleAnnotation, PathToSamplesMap, SampleAnnotationHasher>;
 
-  ProtoContentAnalyzer(TraceProcessorContext* context);
+  explicit ProtoContentAnalyzer(TraceProcessorContext* context);
   ~ProtoContentAnalyzer() override;
 
-  void ProcessPacket(const TraceBlobView& packet,
-                     const SampleAnnotation& annotation) override;
+  void ProcessPacket(const TraceBlobView&, const SampleAnnotation&) override;
 
   void NotifyEndOfFile() override;
 
  private:
   TraceProcessorContext* context_;
-  DescriptorPool pool_;
   util::SizeProfileComputer computer_;
   AnnotatedSamplesMap aggregated_samples_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_CONTENT_ANALYZER_H_
diff --git a/src/trace_processor/importers/proto/default_modules.cc b/src/trace_processor/importers/proto/default_modules.cc
index f7611d7..2e5d397 100644
--- a/src/trace_processor/importers/proto/default_modules.cc
+++ b/src/trace_processor/importers/proto/default_modules.cc
@@ -20,7 +20,6 @@
 #include "src/trace_processor/importers/proto/chrome_system_probes_module.h"
 #include "src/trace_processor/importers/proto/memory_tracker_snapshot_module.h"
 #include "src/trace_processor/importers/proto/metadata_minimal_module.h"
-#include "src/trace_processor/importers/proto/profile_module.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/track_event_module.h"
 
@@ -43,7 +42,6 @@
 
   context->modules.emplace_back(new MemoryTrackerSnapshotModule(context));
   context->modules.emplace_back(new ChromeSystemProbesModule(context));
-  context->modules.emplace_back(new ProfileModule(context));
   context->modules.emplace_back(new MetadataMinimalModule(context));
 }
 
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
index 91d137a..d3f4c45 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.cc
@@ -33,7 +33,6 @@
 #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_compressor.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/common/tracks_common.h"
 #include "src/trace_processor/storage/stats.h"
@@ -195,6 +194,7 @@
           context->storage->InternString("Jank severity type")),
       layer_name_id_(context->storage->InternString("Layer name")),
       prediction_type_id_(context->storage->InternString("Prediction type")),
+      jank_tag_id_(context->storage->InternString("Jank tag")),
       is_buffer_id_(context->storage->InternString("Is Buffer?")),
       jank_tag_none_id_(context->storage->InternString("No Jank")),
       jank_tag_self_id_(context->storage->InternString("Self Jank")),
@@ -223,18 +223,11 @@
       static_cast<uint32_t>(event.pid()));
   cookie_map_[cookie] = std::make_pair(upid, TrackType::kExpected);
 
-  tables::ExpectedFrameTimelineSliceTable::Row expected_row;
-  expected_row.ts = timestamp;
-  expected_row.track_id = context_->track_compressor->InternBegin(
+  TrackId track_id = context_->track_compressor->InternBegin(
       kExpectedBlueprint, tracks::Dimensions(upid), cookie);
-  expected_row.name = name_id;
-
-  expected_row.display_frame_token = token;
-  expected_row.upid = upid;
-
-  context_->slice_tracker->BeginTyped(
-      context_->storage->mutable_expected_frame_timeline_slice_table(),
-      expected_row, [this, token](ArgsTracker::BoundInserter* inserter) {
+  context_->slice_tracker->Begin(
+      timestamp, track_id, kNullStringId, name_id,
+      [this, token](ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
       });
 }
@@ -257,15 +250,8 @@
       static_cast<uint32_t>(event.pid()));
   cookie_map_[cookie] = std::make_pair(upid, TrackType::kActual);
 
-  tables::ActualFrameTimelineSliceTable::Row actual_row;
-  actual_row.ts = timestamp;
-  actual_row.track_id = context_->track_compressor->InternBegin(
+  TrackId track_id = context_->track_compressor->InternBegin(
       kActualBlueprint, tracks::Dimensions(upid), cookie);
-  actual_row.name = name_id;
-  actual_row.display_frame_token = token;
-  actual_row.upid = upid;
-  actual_row.on_time_finish = event.on_time_finish();
-  actual_row.gpu_composition = event.gpu_composition();
 
   // parse present type
   StringId present_type = present_type_ids_[0];
@@ -273,25 +259,22 @@
       ValidatePresentType(context_, event.present_type())) {
     present_type = present_type_ids_[static_cast<size_t>(event.present_type())];
   }
-  actual_row.present_type = present_type;
 
   // parse jank type
   StringId jank_type = JankTypeBitmaskToStringId(context_, event.jank_type());
-  actual_row.jank_type = jank_type;
 
   // parse jank severity type
+  StringId jank_severity_type;
   if (event.has_jank_severity_type()) {
-    actual_row.jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
+    jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
         event.jank_severity_type())];
   } else {
     // NOTE: Older traces don't have this field. If JANK_NONE use
     // |severity_type| "None", and is not present, use "Unknown".
-    actual_row.jank_severity_type =
-        (event.jank_type() == FrameTimelineEvent::JANK_NONE)
-            ? jank_severity_type_ids_[1]  /* None */
-            : jank_severity_type_ids_[0]; /* Unknown */
+    jank_severity_type = (event.jank_type() == FrameTimelineEvent::JANK_NONE)
+                             ? jank_severity_type_ids_[1]  /* None */
+                             : jank_severity_type_ids_[0]; /* Unknown */
   }
-  StringId jank_severity_type = actual_row.jank_severity_type;
 
   // parse prediction type
   StringId prediction_type = prediction_type_ids_[0];
@@ -300,23 +283,21 @@
     prediction_type =
         prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
   }
-  actual_row.prediction_type = prediction_type;
 
+  StringId jank_tag;
   if (DisplayFrameJanky(event.jank_type())) {
-    actual_row.jank_tag = jank_tag_self_id_;
+    jank_tag = jank_tag_self_id_;
   } else if (event.jank_type() == FrameTimelineEvent::JANK_SF_STUFFING) {
-    actual_row.jank_tag = jank_tag_sf_stuffing_id_;
+    jank_tag = jank_tag_sf_stuffing_id_;
   } else if (event.jank_type() == FrameTimelineEvent::JANK_DROPPED) {
-    actual_row.jank_tag = jank_tag_dropped_id_;
+    jank_tag = jank_tag_dropped_id_;
   } else {
-    actual_row.jank_tag = jank_tag_none_id_;
+    jank_tag = jank_tag_none_id_;
   }
 
-  std::optional<SliceId> opt_slice_id = context_->slice_tracker->BeginTyped(
-      context_->storage->mutable_actual_frame_timeline_slice_table(),
-      actual_row,
-      [this, token, jank_type, jank_severity_type, present_type,
-       prediction_type, &event](ArgsTracker::BoundInserter* inserter) {
+  std::optional<SliceId> opt_slice_id = context_->slice_tracker->Begin(
+      timestamp, track_id, kNullStringId, name_id,
+      [&](ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
         inserter->AddArg(present_type_id_, Variadic::String(present_type));
         inserter->AddArg(on_time_finish_id_,
@@ -328,6 +309,7 @@
                          Variadic::String(jank_severity_type));
         inserter->AddArg(prediction_type_id_,
                          Variadic::String(prediction_type));
+        inserter->AddArg(jank_tag_id_, Variadic::String(jank_tag));
       });
 
   // SurfaceFrames will always be parsed before the matching DisplayFrame
@@ -385,21 +367,14 @@
   StringId name_id =
       context_->storage->InternString(base::StringView(std::to_string(token)));
 
-  tables::ExpectedFrameTimelineSliceTable::Row expected_row;
-  expected_row.ts = timestamp;
-  expected_row.track_id = context_->track_compressor->InternBegin(
+  TrackId track_id = context_->track_compressor->InternBegin(
       kExpectedBlueprint, tracks::Dimensions(upid), cookie);
-  expected_row.name = name_id;
-
-  expected_row.surface_frame_token = token;
-  expected_row.display_frame_token = display_frame_token;
-  expected_row.upid = upid;
-  expected_row.layer_name = layer_name_id;
-  context_->slice_tracker->BeginTyped(
-      context_->storage->mutable_expected_frame_timeline_slice_table(),
-      expected_row,
-      [this, token, layer_name_id](ArgsTracker::BoundInserter* inserter) {
-        inserter->AddArg(display_frame_token_id_, Variadic::Integer(token));
+  context_->slice_tracker->Begin(
+      timestamp, track_id, kNullStringId, name_id,
+      [&](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(surface_frame_token_id_, Variadic::Integer(token));
+        inserter->AddArg(display_frame_token_id_,
+                         Variadic::Integer(display_frame_token));
         inserter->AddArg(layer_name_id_, Variadic::String(layer_name_id));
       });
 }
@@ -429,17 +404,8 @@
   StringId name_id =
       context_->storage->InternString(base::StringView(std::to_string(token)));
 
-  tables::ActualFrameTimelineSliceTable::Row actual_row;
-  actual_row.ts = timestamp;
-  actual_row.track_id = context_->track_compressor->InternBegin(
+  TrackId track_id = context_->track_compressor->InternBegin(
       kActualBlueprint, tracks::Dimensions(upid), cookie);
-  actual_row.name = name_id;
-  actual_row.surface_frame_token = token;
-  actual_row.display_frame_token = display_frame_token;
-  actual_row.upid = upid;
-  actual_row.layer_name = layer_name_id;
-  actual_row.on_time_finish = event.on_time_finish();
-  actual_row.gpu_composition = event.gpu_composition();
 
   // parse present type
   StringId present_type = present_type_ids_[0];
@@ -449,25 +415,22 @@
     present_type_validated = true;
     present_type = present_type_ids_[static_cast<size_t>(event.present_type())];
   }
-  actual_row.present_type = present_type;
 
   // parse jank type
   StringId jank_type = JankTypeBitmaskToStringId(context_, event.jank_type());
-  actual_row.jank_type = jank_type;
 
   // parse jank severity type
+  StringId jank_severity_type;
   if (event.has_jank_severity_type()) {
-    actual_row.jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
+    jank_severity_type = jank_severity_type_ids_[static_cast<size_t>(
         event.jank_severity_type())];
   } else {
     // NOTE: Older traces don't have this field. If JANK_NONE use
     // |severity_type| "None", and is not present, use "Unknown".
-    actual_row.jank_severity_type =
-        (event.jank_type() == FrameTimelineEvent::JANK_NONE)
-            ? jank_severity_type_ids_[1]  /* None */
-            : jank_severity_type_ids_[0]; /* Unknown */
+    jank_severity_type = (event.jank_type() == FrameTimelineEvent::JANK_NONE)
+                             ? jank_severity_type_ids_[1]  /* None */
+                             : jank_severity_type_ids_[0]; /* Unknown */
   }
-  StringId jank_severity_type = actual_row.jank_severity_type;
 
   // parse prediction type
   StringId prediction_type = prediction_type_ids_[0];
@@ -476,34 +439,32 @@
     prediction_type =
         prediction_type_ids_[static_cast<size_t>(event.prediction_type())];
   }
-  actual_row.prediction_type = prediction_type;
 
+  StringId jank_tag;
   if (SurfaceFrameJanky(event.jank_type())) {
-    actual_row.jank_tag = jank_tag_self_id_;
+    jank_tag = jank_tag_self_id_;
   } else if (DisplayFrameJanky(event.jank_type())) {
-    actual_row.jank_tag = jank_tag_other_id_;
+    jank_tag = jank_tag_other_id_;
   } else if (event.jank_type() == FrameTimelineEvent::JANK_BUFFER_STUFFING) {
-    actual_row.jank_tag = jank_tag_buffer_stuffing_id_;
+    jank_tag = jank_tag_buffer_stuffing_id_;
   } else if (present_type_validated &&
              event.present_type() == FrameTimelineEvent::PRESENT_DROPPED) {
-    actual_row.jank_tag = jank_tag_dropped_id_;
+    jank_tag = jank_tag_dropped_id_;
   } else {
-    actual_row.jank_tag = jank_tag_none_id_;
+    jank_tag = jank_tag_none_id_;
   }
   StringId is_buffer = context_->storage->InternString("Unspecified");
   if (event.has_is_buffer()) {
-    if (event.is_buffer())
+    if (event.is_buffer()) {
       is_buffer = context_->storage->InternString("Yes");
-    else
+    } else {
       is_buffer = context_->storage->InternString("No");
+    }
   }
 
-  std::optional<SliceId> opt_slice_id = context_->slice_tracker->BeginTyped(
-      context_->storage->mutable_actual_frame_timeline_slice_table(),
-      actual_row,
-      [this, jank_type, jank_severity_type, present_type, token, layer_name_id,
-       display_frame_token, prediction_type, is_buffer,
-       &event](ArgsTracker::BoundInserter* inserter) {
+  std::optional<SliceId> opt_slice_id = context_->slice_tracker->Begin(
+      timestamp, track_id, kNullStringId, name_id,
+      [&](ArgsTracker::BoundInserter* inserter) {
         inserter->AddArg(surface_frame_token_id_, Variadic::Integer(token));
         inserter->AddArg(display_frame_token_id_,
                          Variadic::Integer(display_frame_token));
@@ -518,6 +479,7 @@
                          Variadic::String(jank_severity_type));
         inserter->AddArg(prediction_type_id_,
                          Variadic::String(prediction_type));
+        inserter->AddArg(jank_tag_id_, Variadic::String(jank_tag));
         inserter->AddArg(is_buffer_id_, Variadic::String(is_buffer));
       });
 
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.h b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
index 30ab79b..f52508d 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.h
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
@@ -74,23 +74,24 @@
   std::array<StringId, 4> prediction_type_ids_;
   std::array<StringId, 4> jank_severity_type_ids_;
 
-  StringId surface_frame_token_id_;
-  StringId display_frame_token_id_;
-  StringId present_type_id_;
-  StringId on_time_finish_id_;
-  StringId gpu_composition_id_;
-  StringId jank_type_id_;
-  StringId jank_severity_type_id_;
-  StringId layer_name_id_;
-  StringId prediction_type_id_;
-  StringId is_buffer_id_;
+  const StringId surface_frame_token_id_;
+  const StringId display_frame_token_id_;
+  const StringId present_type_id_;
+  const StringId on_time_finish_id_;
+  const StringId gpu_composition_id_;
+  const StringId jank_type_id_;
+  const StringId jank_severity_type_id_;
+  const StringId layer_name_id_;
+  const StringId prediction_type_id_;
+  const StringId jank_tag_id_;
+  const StringId is_buffer_id_;
 
-  StringId jank_tag_none_id_;
-  StringId jank_tag_self_id_;
-  StringId jank_tag_other_id_;
-  StringId jank_tag_dropped_id_;
-  StringId jank_tag_buffer_stuffing_id_;
-  StringId jank_tag_sf_stuffing_id_;
+  const StringId jank_tag_none_id_;
+  const StringId jank_tag_self_id_;
+  const StringId jank_tag_other_id_;
+  const StringId jank_tag_dropped_id_;
+  const StringId jank_tag_buffer_stuffing_id_;
+  const StringId jank_tag_sf_stuffing_id_;
 
   // upid -> set of tokens map. The expected timeline is the same for a given
   // token no matter how many times its seen. We can safely ignore duplicates
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.cc b/src/trace_processor/importers/proto/gpu_event_parser.cc
index b725ae6..2070e53 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.cc
+++ b/src/trace_processor/importers/proto/gpu_event_parser.cc
@@ -20,7 +20,6 @@
 #include <cinttypes>
 #include <cstddef>
 #include <cstdint>
-#include <limits>
 #include <optional>
 #include <string>
 #include <vector>
@@ -43,7 +42,6 @@
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/profiler_tables_py.h"
-#include "src/trace_processor/tables/slice_tables_py.h"
 #include "src/trace_processor/tables/track_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
@@ -121,21 +119,27 @@
         tracks::StringDimensionBlueprint("hwqueue_name")),
     tracks::DynamicNameBlueprint());
 
-constexpr auto kVulkanEventsBlueprint =
-    tracks::SliceBlueprint("vulkan_events",
-                           tracks::DimensionBlueprints(),
-                           tracks::StaticNameBlueprint("Vulkan Events"));
-
-constexpr auto kGpuLogBlueprint =
-    tracks::SliceBlueprint("gpu_log",
-                           tracks::DimensionBlueprints(),
-                           tracks::StaticNameBlueprint("GPU Log"));
-
 }  // anonymous namespace
 
 GpuEventParser::GpuEventParser(TraceProcessorContext* context)
     : context_(context),
       vulkan_memory_tracker_(context),
+      context_id_id_(context->storage->InternString("context_id")),
+      render_target_id_(context->storage->InternString("render_target")),
+      render_target_name_id_(
+          context->storage->InternString("render_target_name")),
+      render_pass_id_(context->storage->InternString("render_pass")),
+      render_pass_name_id_(context->storage->InternString("render_pass_name")),
+      render_subpasses_id_(context->storage->InternString("render_subpasses")),
+      command_buffer_id_(context->storage->InternString("command_buffer")),
+      command_buffer_name_id_(
+          context->storage->InternString("command_buffer_name")),
+      frame_id_id_(context->storage->InternString("frame_id")),
+      submission_id_id_(context->storage->InternString("submission_id")),
+      hw_queue_id_id_(context->storage->InternString("hw_queue_id")),
+      upid_id_(context->storage->InternString("upid")),
+      pid_id_(context_->storage->InternString("pid")),
+      tid_id_(context_->storage->InternString("tid")),
       description_id_(context->storage->InternString("description")),
       tag_id_(context_->storage->InternString("tag")),
       log_message_id_(context->storage->InternString("message")),
@@ -385,37 +389,6 @@
     }
   }
 
-  auto args_callback = [this, &event,
-                        sequence_state](ArgsTracker::BoundInserter* inserter) {
-    if (event.has_stage_iid()) {
-      size_t stage_iid = static_cast<size_t>(event.stage_iid());
-      auto* decoder = sequence_state->LookupInternedMessage<
-          protos::pbzero::InternedData::kGpuSpecificationsFieldNumber,
-          protos::pbzero::InternedGpuRenderStageSpecification>(stage_iid);
-      if (decoder) {
-        // TODO: Add RenderStageCategory to gpu_slice table.
-        inserter->AddArg(description_id_,
-                         Variadic::String(context_->storage->InternString(
-                             decoder->description())));
-      }
-    } else if (event.has_stage_id()) {
-      size_t stage_id = static_cast<size_t>(event.stage_id());
-      if (stage_id < gpu_render_stage_ids_.size()) {
-        auto description = gpu_render_stage_ids_[stage_id].second;
-        if (description != kNullStringId) {
-          inserter->AddArg(description_id_, Variadic::String(description));
-        }
-      }
-    }
-    for (auto it = event.extra_data(); it; ++it) {
-      protos::pbzero::GpuRenderStageEvent_ExtraData_Decoder datum(*it);
-      StringId name_id = context_->storage->InternString(datum.name());
-      StringId value = context_->storage->InternString(
-          datum.has_value() ? datum.value() : base::StringView());
-      inserter->AddArg(name_id, Variadic::String(value));
-    }
-  };
-
   if (event.has_event_id()) {
     TrackId track_id;
     uint64_t hw_queue_id = 0;
@@ -428,7 +401,7 @@
         // Skip
         return;
       }
-      // TODO: Add RenderStageCategory to gpu_track table.
+      // TODO: Add RenderStageCategory to track table.
       track_id = context_->track_tracker->InternTrack(
           kRenderStageBlueprint,
           tracks::Dimensions("iid", hw_queue_id, decoder->name()),
@@ -484,28 +457,78 @@
                                       ? context_->storage->InternString(
                                             command_buffer_name.value().c_str())
                                       : kNullStringId;
+    StringId name_id;
+    if (event.has_submission_id()) {
+      name_id = context_->storage->InternString(
+          std::to_string(event.submission_id()).c_str());
+    } else {
+      name_id = GetFullStageName(sequence_state, event);
+    }
+    context_->slice_tracker->Scoped(
+        ts, track_id, kNullStringId, name_id,
+        static_cast<int64_t>(event.duration()),
+        [&](ArgsTracker::BoundInserter* inserter) {
+          if (event.has_stage_iid()) {
+            auto stage_iid = static_cast<size_t>(event.stage_iid());
+            auto* decoder = sequence_state->LookupInternedMessage<
+                protos::pbzero::InternedData::kGpuSpecificationsFieldNumber,
+                protos::pbzero::InternedGpuRenderStageSpecification>(stage_iid);
+            if (decoder) {
+              // TODO: Add RenderStageCategory to gpu_slice table.
+              inserter->AddArg(description_id_,
+                               Variadic::String(context_->storage->InternString(
+                                   decoder->description())));
+            }
+          } else if (event.has_stage_id()) {
+            size_t stage_id = static_cast<size_t>(event.stage_id());
+            if (stage_id < gpu_render_stage_ids_.size()) {
+              auto description = gpu_render_stage_ids_[stage_id].second;
+              if (description != kNullStringId) {
+                inserter->AddArg(description_id_,
+                                 Variadic::String(description));
+              }
+            }
+          }
+          for (auto it = event.extra_data(); it; ++it) {
+            protos::pbzero::GpuRenderStageEvent_ExtraData_Decoder datum(*it);
+            StringId name_id = context_->storage->InternString(datum.name());
+            StringId value = context_->storage->InternString(
+                datum.has_value() ? datum.value() : base::StringView());
+            inserter->AddArg(name_id, Variadic::String(value));
+          }
 
-    tables::GpuSliceTable::Row row;
-    row.ts = ts;
-    row.track_id = track_id;
-    row.name = GetFullStageName(sequence_state, event);
-    row.dur = static_cast<int64_t>(event.duration());
-    // TODO: Create table for graphics context and lookup
-    // InternedGraphicsContext.
-    row.context_id = static_cast<int64_t>(event.context());
-    row.render_target = static_cast<int64_t>(event.render_target_handle());
-    row.render_target_name = render_target_name_id;
-    row.render_pass = static_cast<int64_t>(event.render_pass_handle());
-    row.render_pass_name = render_pass_name_id;
-    row.render_subpasses = ParseRenderSubpasses(event);
-    row.command_buffer = static_cast<int64_t>(event.command_buffer_handle());
-    row.command_buffer_name = command_buffer_name_id;
-    row.submission_id = event.submission_id();
-    row.hw_queue_id = static_cast<int64_t>(hw_queue_id);
-    row.upid = context_->process_tracker->GetOrCreateProcess(
-        static_cast<uint32_t>(pid));
-    context_->slice_tracker->ScopedTyped(
-        context_->storage->mutable_gpu_slice_table(), row, args_callback);
+          // TODO: Create table for graphics context and lookup
+          // InternedGraphicsContext.
+          inserter->AddArg(
+              context_id_id_,
+              Variadic::Integer(static_cast<int64_t>(event.context())));
+          inserter->AddArg(render_target_id_,
+                           Variadic::Integer(static_cast<int64_t>(
+                               event.render_target_handle())));
+          inserter->AddArg(render_target_name_id_,
+                           Variadic::String(render_target_name_id));
+          inserter->AddArg(render_pass_id_,
+                           Variadic::Integer(static_cast<int64_t>(
+                               event.render_pass_handle())));
+          inserter->AddArg(render_pass_name_id_,
+                           Variadic::String(render_pass_name_id));
+          inserter->AddArg(render_subpasses_id_,
+                           Variadic::String(ParseRenderSubpasses(event)));
+          inserter->AddArg(command_buffer_id_,
+                           Variadic::Integer(static_cast<int64_t>(
+                               event.command_buffer_handle())));
+          inserter->AddArg(command_buffer_name_id_,
+                           Variadic::String(command_buffer_name_id));
+          inserter->AddArg(submission_id_id_,
+                           Variadic::Integer(event.submission_id()));
+          inserter->AddArg(
+              hw_queue_id_id_,
+              Variadic::Integer(static_cast<int64_t>(hw_queue_id)));
+          inserter->AddArg(
+              upid_id_,
+              Variadic::Integer(context_->process_tracker->GetOrCreateProcess(
+                  static_cast<uint32_t>(pid))));
+        });
   }
 }
 
@@ -723,20 +746,17 @@
 void GpuEventParser::ParseGpuLog(int64_t ts, ConstBytes blob) {
   protos::pbzero::GpuLog::Decoder event(blob);
 
+  static constexpr auto kGpuLogBlueprint =
+      tracks::SliceBlueprint("gpu_log", tracks::DimensionBlueprints(),
+                             tracks::StaticNameBlueprint("GPU Log"));
   TrackId track_id = context_->track_tracker->InternTrack(kGpuLogBlueprint);
   auto severity = static_cast<size_t>(event.severity());
   StringId severity_id =
       severity < log_severity_ids_.size()
           ? log_severity_ids_[static_cast<size_t>(event.severity())]
           : log_severity_ids_[log_severity_ids_.size() - 1];
-
-  tables::GpuSliceTable::Row row;
-  row.ts = ts;
-  row.track_id = track_id;
-  row.name = severity_id;
-  row.dur = 0;
-  context_->slice_tracker->ScopedTyped(
-      context_->storage->mutable_gpu_slice_table(), row,
+  context_->slice_tracker->Scoped(
+      ts, track_id, kNullStringId, severity_id, 0,
       [this, &event](ArgsTracker::BoundInserter* inserter) {
         if (event.has_tag()) {
           inserter->AddArg(
@@ -759,32 +779,33 @@
     debug_marker_names_[event.object_type()][event.object()] =
         event.object_name().ToStdString();
   }
-  if (vk_event.has_vk_queue_submit()) {
-    protos::pbzero::VulkanApiEvent_VkQueueSubmit::Decoder event(
-        vk_event.vk_queue_submit());
-    // Once flow table is implemented, we can create a nice UI that link the
-    // vkQueueSubmit to GpuRenderStageEvent.  For now, just add it as in a GPU
-    // track so that they can appear close to the render stage slices.
-    TrackId track_id =
-        context_->track_tracker->InternTrack(kVulkanEventsBlueprint);
-    tables::GpuSliceTable::Row row;
-    row.ts = ts;
-    row.dur = static_cast<int64_t>(event.duration_ns());
-    row.track_id = track_id;
-    row.name = vk_queue_submit_id_;
-    if (event.has_vk_command_buffers()) {
-      row.command_buffer = static_cast<int64_t>(*event.vk_command_buffers());
-    }
-    row.submission_id = event.submission_id();
-    auto args_callback = [this, &event](ArgsTracker::BoundInserter* inserter) {
-      inserter->AddArg(context_->storage->InternString("pid"),
-                       Variadic::Integer(event.pid()));
-      inserter->AddArg(context_->storage->InternString("tid"),
-                       Variadic::Integer(event.tid()));
-    };
-    context_->slice_tracker->ScopedTyped(
-        context_->storage->mutable_gpu_slice_table(), row, args_callback);
+  if (!vk_event.has_vk_queue_submit()) {
+    return;
   }
+  protos::pbzero::VulkanApiEvent_VkQueueSubmit::Decoder event(
+      vk_event.vk_queue_submit());
+  // Once flow table is implemented, we can create a nice UI that link the
+  // vkQueueSubmit to GpuRenderStageEvent.  For now, just add it as in a GPU
+  // track so that they can appear close to the render stage slices.
+  static constexpr auto kVulkanEventsBlueprint =
+      tracks::SliceBlueprint("vulkan_events", tracks::DimensionBlueprints(),
+                             tracks::StaticNameBlueprint("Vulkan Events"));
+  TrackId track_id =
+      context_->track_tracker->InternTrack(kVulkanEventsBlueprint);
+  context_->slice_tracker->Scoped(
+      ts, track_id, kNullStringId, vk_queue_submit_id_,
+      static_cast<int64_t>(event.duration_ns()),
+      [this, &event](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(pid_id_, Variadic::Integer(event.pid()));
+        inserter->AddArg(tid_id_, Variadic::Integer(event.tid()));
+        if (event.has_vk_command_buffers()) {
+          inserter->AddArg(command_buffer_id_,
+                           Variadic::Integer(static_cast<int64_t>(
+                               *event.vk_command_buffers())));
+        }
+        inserter->AddArg(submission_id_id_,
+                         Variadic::Integer(event.submission_id()));
+      });
 }
 
 void GpuEventParser::ParseGpuMemTotalEvent(int64_t ts, ConstBytes blob) {
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.h b/src/trace_processor/importers/proto/gpu_event_parser.h
index 11bcb76..324b066 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.h
+++ b/src/trace_processor/importers/proto/gpu_event_parser.h
@@ -91,13 +91,32 @@
 
   TraceProcessorContext* const context_;
   VulkanMemoryTracker vulkan_memory_tracker_;
+
+  const StringId context_id_id_;
+  const StringId render_target_id_;
+  const StringId render_target_name_id_;
+  const StringId render_pass_id_;
+  const StringId render_pass_name_id_;
+  const StringId render_subpasses_id_;
+  const StringId command_buffer_id_;
+  const StringId command_buffer_name_id_;
+  const StringId frame_id_id_;
+  const StringId submission_id_id_;
+  const StringId hw_queue_id_id_;
+  const StringId upid_id_;
+  const StringId pid_id_;
+  const StringId tid_id_;
+
   // For GpuCounterEvent
   std::unordered_map<uint32_t, TrackId> gpu_counter_track_ids_;
+
   // For GpuRenderStageEvent
   const StringId description_id_;
   std::vector<std::optional<TrackId>> gpu_hw_queue_ids_;
+
   // Map of stage ID -> pair(stage name, stage description)
   std::vector<std::pair<StringId, StringId>> gpu_render_stage_ids_;
+
   // For VulkanMemoryEvent
   std::unordered_map<protos::pbzero::VulkanMemoryEvent::AllocationScope,
                      int64_t /*counter_value*/,
@@ -107,16 +126,20 @@
       vulkan_device_memory_counters_allocate_;
   std::unordered_map<uint32_t /*memory_type*/, int64_t /*counter_value*/>
       vulkan_device_memory_counters_bind_;
+
   // For GpuLog
   const StringId tag_id_;
   const StringId log_message_id_;
   std::array<StringId, 7> log_severity_ids_;
+
   // For Vulkan events.
   // For VulkanApiEvent.VkDebugUtilsObjectName.
   // Map of vk handle -> vk object name.
   using DebugMarkerMap = std::unordered_map<uint64_t, std::string>;
+
   // Map of VkObjectType -> DebugMarkerMap.
   std::unordered_map<int32_t, DebugMarkerMap> debug_marker_names_;
+
   // For VulkanApiEvent.VkQueueSubmit.
   StringId vk_event_track_id_;
   StringId vk_queue_submit_id_;
diff --git a/src/trace_processor/importers/proto/graphics_frame_event_parser.cc b/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
index 8506637..077c8c2 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.cc
@@ -20,11 +20,14 @@
 #include <cstddef>
 #include <cstdint>
 #include <optional>
+#include <string>
+#include <utility>
+#include <variant>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/string_writer.h"
-#include "perfetto/ext/base/utils.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
@@ -33,8 +36,8 @@
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
-#include "src/trace_processor/tables/track_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
 
 #include "protos/perfetto/trace/android/graphics_frame_event.pbzero.h"
 
@@ -59,6 +62,14 @@
       unknown_event_name_id_(context->storage->InternString("unknown_event")),
       no_layer_name_name_id_(context->storage->InternString("no_layer_name")),
       layer_name_key_id_(context->storage->InternString("layer_name")),
+      queue_lost_message_id_(context->storage->InternString(kQueueLostMessage)),
+      frame_number_id_(context->storage->InternString("frame_number")),
+      queue_to_acquire_time_id_(
+          context->storage->InternString("queue_to_acquire_time")),
+      acquire_to_latch_time_id_(
+          context->storage->InternString("acquire_to_latch_time")),
+      latch_to_present_time_id_(
+          context->storage->InternString("latch_to_present_time")),
       event_type_name_ids_{
           {context->storage->InternString(
                "unspecified_event") /* UNSPECIFIED */,
@@ -79,338 +90,7 @@
            context->storage->InternString("Modify") /* MODIFY */,
            context->storage->InternString("Detach") /* DETACH */,
            context->storage->InternString("Attach") /* ATTACH */,
-           context->storage->InternString("Cancel") /* CANCEL */}},
-      queue_lost_message_id_(
-          context->storage->InternString(kQueueLostMessage)) {}
-
-bool GraphicsFrameEventParser::CreateBufferEvent(
-    int64_t timestamp,
-    GraphicsFrameEventDecoder& event) {
-  if (!event.has_buffer_id()) {
-    context_->storage->IncrementStats(
-        stats::graphics_frame_event_parser_errors);
-    PERFETTO_ELOG("GraphicsFrameEvent with missing buffer id field.");
-    return false;
-  }
-
-  // Use buffer id + layer name as key because sometimes the same buffer can be
-  // used by different layers.
-  char event_key_buffer[4096];
-  base::StringWriter event_key_str(event_key_buffer,
-                                   base::ArraySize(event_key_buffer));
-  const uint32_t buffer_id = event.buffer_id();
-  StringId layer_name_id;
-  event_key_str.AppendUnsignedInt(buffer_id);
-
-  if (event.has_layer_name()) {
-    layer_name_id = context_->storage->InternString(event.layer_name());
-    event_key_str.AppendString(base::StringView(event.layer_name()));
-  } else {
-    layer_name_id = no_layer_name_name_id_;
-  }
-  StringId event_key =
-      context_->storage->InternString(event_key_str.GetStringView());
-
-  StringId event_name_id = unknown_event_name_id_;
-  if (event.has_type()) {
-    const auto type = static_cast<size_t>(event.type());
-    if (type < event_type_name_ids_.size()) {
-      event_name_id = event_type_name_ids_[type];
-      graphics_frame_stats_map_[event_key][type] = timestamp;
-    } else {
-      context_->storage->IncrementStats(
-          stats::graphics_frame_event_parser_errors);
-      PERFETTO_ELOG("GraphicsFrameEvent with unknown type %zu.", type);
-    }
-  } else {
-    context_->storage->IncrementStats(
-        stats::graphics_frame_event_parser_errors);
-    PERFETTO_ELOG("GraphicsFrameEvent with missing type field.");
-  }
-
-  char buffer[4096];
-  base::StringWriter track_name(buffer, base::ArraySize(buffer));
-  track_name.AppendLiteral("Buffer: ");
-  track_name.AppendUnsignedInt(buffer_id);
-  track_name.AppendLiteral(" ");
-  track_name.AppendString(base::StringView(event.layer_name()));
-
-  const int64_t duration =
-      event.has_duration_ns() ? static_cast<int64_t>(event.duration_ns()) : 0;
-  uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
-
-  TrackId track_id = context_->track_tracker->InternTrack(
-      kGraphicFrameEventBlueprint,
-      tracks::Dimensions(track_name.GetStringView()),
-      tracks::DynamicName(
-          context_->storage->InternString(track_name.GetStringView())));
-
-  auto* graphics_frame_slice_table =
-      context_->storage->mutable_graphics_frame_slice_table();
-  {
-    tables::GraphicsFrameSliceTable::Row row;
-    row.ts = timestamp;
-    row.track_id = track_id;
-    row.name = event_name_id;
-    row.dur = duration;
-    row.frame_number = frame_number;
-    row.layer_name = layer_name_id;
-    if (event.type() == GraphicsFrameEvent::PRESENT_FENCE) {
-      auto acquire_ts =
-          graphics_frame_stats_map_[event_key]
-                                   [GraphicsFrameEvent::ACQUIRE_FENCE];
-      auto queue_ts =
-          graphics_frame_stats_map_[event_key][GraphicsFrameEvent::QUEUE];
-      auto latch_ts =
-          graphics_frame_stats_map_[event_key][GraphicsFrameEvent::LATCH];
-
-      row.queue_to_acquire_time =
-          std::max(acquire_ts - queue_ts, static_cast<int64_t>(0));
-      row.acquire_to_latch_time = latch_ts - acquire_ts;
-      row.latch_to_present_time = timestamp - latch_ts;
-    }
-    std::optional<SliceId> opt_slice_id =
-        context_->slice_tracker->ScopedTyped(graphics_frame_slice_table, row);
-    if (event.type() == GraphicsFrameEvent::DEQUEUE) {
-      if (opt_slice_id) {
-        dequeue_slice_ids_[event_key] = *opt_slice_id;
-      }
-    } else if (event.type() == GraphicsFrameEvent::QUEUE) {
-      auto it = dequeue_slice_ids_.find(event_key);
-      if (it != dequeue_slice_ids_.end()) {
-        auto rr = graphics_frame_slice_table->FindById(it->second);
-        rr->set_frame_number(frame_number);
-      }
-    }
-  }
-  return true;
-}
-
-void GraphicsFrameEventParser::InvalidatePhaseEvent(int64_t timestamp,
-                                                    TrackId track_id,
-                                                    bool reset_name) {
-  const auto opt_slice_id = context_->slice_tracker->End(timestamp, track_id);
-
-  if (opt_slice_id) {
-    auto* graphics_frame_slice_table =
-        context_->storage->mutable_graphics_frame_slice_table();
-    auto rr = *graphics_frame_slice_table->FindById(*opt_slice_id);
-    if (reset_name) {
-      // Set the name (frame_number) to be 0 since there is no frame number
-      // associated, example : dequeue event.
-      StringId frame_name_id = context_->storage->InternString("0");
-      rr.set_name(frame_name_id);
-      rr.set_frame_number(0);
-    }
-
-    // Set the duration to -1 so that this slice will be ignored by the
-    // UI. Setting any other duration results in wrong data which we want
-    // to avoid at all costs.
-    rr.set_dur(-1);
-  }
-}
-
-// Here we convert the buffer events into Phases(slices)
-// APP: Dequeue to Queue
-// Wait for GPU: Queue to Acquire
-// SurfaceFlinger (SF): Latch to Present
-// Display: Present to next Present (of the same layer)
-void GraphicsFrameEventParser::CreatePhaseEvent(
-    int64_t timestamp,
-    GraphicsFrameEventDecoder& event) {
-  // Use buffer id + layer name as key because sometimes the same buffer can be
-  // used by different layers.
-  char event_key_buffer[4096];
-  base::StringWriter event_key_str(event_key_buffer,
-                                   base::ArraySize(event_key_buffer));
-  const uint32_t buffer_id = event.buffer_id();
-  uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
-  event_key_str.AppendUnsignedInt(buffer_id);
-  StringId layer_name_id;
-  if (event.has_layer_name()) {
-    layer_name_id = context_->storage->InternString(event.layer_name());
-    event_key_str.AppendString(base::StringView(event.layer_name()));
-  } else {
-    layer_name_id = no_layer_name_name_id_;
-  }
-  StringId event_key =
-      context_->storage->InternString(event_key_str.GetStringView());
-
-  char track_buffer[4096];
-  char slice_buffer[4096];
-  // We'll be using the name StringWriter and name_id for writing track names
-  // and slice names.
-  base::StringWriter track_name(track_buffer, base::ArraySize(track_buffer));
-  base::StringWriter slice_name(slice_buffer, base::ArraySize(slice_buffer));
-  StringId track_name_id;
-  TrackId track_id;
-  bool start_slice = true;
-
-  // Close the previous phase before starting the new phase
-  switch (event.type()) {
-    case GraphicsFrameEvent::DEQUEUE: {
-      track_name.reset();
-      track_name.AppendLiteral("APP_");
-      track_name.AppendUnsignedInt(buffer_id);
-      track_name.AppendLiteral(" ");
-      track_name.AppendString(base::StringView(event.layer_name()));
-      track_name_id =
-          context_->storage->InternString(track_name.GetStringView());
-
-      track_id = context_->track_tracker->InternTrack(
-          kGraphicFrameEventBlueprint,
-          tracks::Dimensions(track_name.GetStringView()),
-          tracks::DynamicName(
-              context_->storage->InternString(track_name.GetStringView())));
-
-      // Error handling
-      auto dequeue_time = dequeue_map_.find(event_key);
-      if (dequeue_time != dequeue_map_.end()) {
-        InvalidatePhaseEvent(timestamp, dequeue_time->second, true);
-        dequeue_map_.erase(dequeue_time);
-      }
-      auto queue_time = queue_map_.find(event_key);
-      if (queue_time != queue_map_.end()) {
-        InvalidatePhaseEvent(timestamp, queue_time->second);
-        queue_map_.erase(queue_time);
-      }
-
-      dequeue_map_[event_key] = track_id;
-      last_dequeued_[event_key] = timestamp;
-      break;
-    }
-
-    case GraphicsFrameEvent::QUEUE: {
-      auto dequeue_time = dequeue_map_.find(event_key);
-      if (dequeue_time != dequeue_map_.end()) {
-        const auto opt_slice_id =
-            context_->slice_tracker->End(timestamp, dequeue_time->second);
-        slice_name.reset();
-        slice_name.AppendUnsignedInt(frame_number);
-        if (opt_slice_id) {
-          auto* graphics_frame_slice_table =
-              context_->storage->mutable_graphics_frame_slice_table();
-          // Set the name of the slice to be the frame number since dequeue did
-          // not have a frame number at that time.
-          auto rr = *graphics_frame_slice_table->FindById(*opt_slice_id);
-          rr.set_name(
-              context_->storage->InternString(slice_name.GetStringView()));
-          rr.set_frame_number(frame_number);
-          dequeue_map_.erase(dequeue_time);
-        }
-      }
-      // The AcquireFence might be signaled before receiving a QUEUE event
-      // sometimes. In that case, we shouldn't start a slice.
-      if (last_acquired_[event_key] > last_dequeued_[event_key] &&
-          last_acquired_[event_key] < timestamp) {
-        start_slice = false;
-        break;
-      }
-      track_name.reset();
-      track_name.AppendLiteral("GPU_");
-      track_name.AppendUnsignedInt(buffer_id);
-      track_name.AppendLiteral(" ");
-      track_name.AppendString(base::StringView(event.layer_name()));
-      track_name_id =
-          context_->storage->InternString(track_name.GetStringView());
-
-      track_id = context_->track_tracker->InternTrack(
-          kGraphicFrameEventBlueprint,
-          tracks::Dimensions(track_name.GetStringView()),
-          tracks::DynamicName(
-              context_->storage->InternString(track_name.GetStringView())));
-      queue_map_[event_key] = track_id;
-      break;
-    }
-    case GraphicsFrameEvent::ACQUIRE_FENCE: {
-      auto queue_time = queue_map_.find(event_key);
-      if (queue_time != queue_map_.end()) {
-        context_->slice_tracker->End(timestamp, queue_time->second);
-        queue_map_.erase(queue_time);
-      }
-      last_acquired_[event_key] = timestamp;
-      start_slice = false;
-      break;
-    }
-    case GraphicsFrameEvent::LATCH: {
-      // b/157578286 - Sometimes Queue event goes missing. To prevent having a
-      // wrong slice info, we try to close any existing APP slice.
-      auto dequeue_time = dequeue_map_.find(event_key);
-      if (dequeue_time != dequeue_map_.end()) {
-        InvalidatePhaseEvent(timestamp, dequeue_time->second, true);
-        dequeue_map_.erase(dequeue_time);
-      }
-      track_name.reset();
-      track_name.AppendLiteral("SF_");
-      track_name.AppendUnsignedInt(buffer_id);
-      track_name.AppendLiteral(" ");
-      track_name.AppendString(base::StringView(event.layer_name()));
-      track_name_id =
-          context_->storage->InternString(track_name.GetStringView());
-
-      track_id = context_->track_tracker->InternTrack(
-          kGraphicFrameEventBlueprint,
-          tracks::Dimensions(track_name.GetStringView()),
-          tracks::DynamicName(
-              context_->storage->InternString(track_name.GetStringView())));
-      latch_map_[event_key] = track_id;
-      break;
-    }
-
-    case GraphicsFrameEvent::PRESENT_FENCE: {
-      auto latch_time = latch_map_.find(event_key);
-      if (latch_time != latch_map_.end()) {
-        context_->slice_tracker->End(timestamp, latch_time->second);
-        latch_map_.erase(latch_time);
-      }
-      auto display_time = display_map_.find(layer_name_id);
-      if (display_time != display_map_.end()) {
-        context_->slice_tracker->End(timestamp, display_time->second);
-        display_map_.erase(display_time);
-      }
-      base::StringView layerName(event.layer_name());
-      track_name.reset();
-      track_name.AppendLiteral("Display_");
-      track_name.AppendString(layerName.substr(0, 10));
-      track_name_id =
-          context_->storage->InternString(track_name.GetStringView());
-
-      track_id = context_->track_tracker->InternTrack(
-          kGraphicFrameEventBlueprint,
-          tracks::Dimensions(track_name.GetStringView()),
-          tracks::DynamicName(
-              context_->storage->InternString(track_name.GetStringView())));
-      display_map_[layer_name_id] = track_id;
-      break;
-    }
-
-    default:
-      start_slice = false;
-  }
-
-  // Start the new phase if needed.
-  if (start_slice) {
-    tables::GraphicsFrameSliceTable::Row slice;
-    slice.ts = timestamp;
-    slice.track_id = track_id;
-    slice.layer_name = layer_name_id;
-    slice_name.reset();
-    // If the frame_number is known, set it as the name of the slice.
-    // If not known (DEQUEUE), set the name as the timestamp.
-    // Timestamp is chosen here because the stack_id is hashed based on the name
-    // of the slice. To not have any conflicting stack_id with any of the
-    // existing slices, we use timestamp as the temporary name.
-    if (frame_number != 0) {
-      slice_name.AppendUnsignedInt(frame_number);
-    } else {
-      slice_name.AppendInt(timestamp);
-    }
-    slice.name = context_->storage->InternString(slice_name.GetStringView());
-    slice.frame_number = frame_number;
-    context_->slice_tracker->BeginTyped(
-        context_->storage->mutable_graphics_frame_slice_table(), slice);
-  }
-}
+           context->storage->InternString("Cancel") /* CANCEL */}} {}
 
 void GraphicsFrameEventParser::ParseGraphicsFrameEvent(int64_t timestamp,
                                                        ConstBytes blob) {
@@ -421,10 +101,267 @@
 
   protos::pbzero::GraphicsFrameEvent::BufferEvent::Decoder event(
       frame_event.buffer_event());
-  if (CreateBufferEvent(timestamp, event)) {
-    // Create a phase event only if the buffer event finishes successfully
-    CreatePhaseEvent(timestamp, event);
+  if (!event.has_buffer_id()) {
+    context_->storage->IncrementStats(
+        stats::graphics_frame_event_parser_errors);
+    return;
   }
+
+  // Use buffer id + layer name as key because sometimes the same buffer can be
+  // used by different layers.
+  StringId layer_name_id;
+  StringId event_key;
+  if (event.has_layer_name()) {
+    layer_name_id = context_->storage->InternString(event.layer_name());
+    base::StackString<1024> key_str("%u%.*s", event.buffer_id(),
+                                    int(event.layer_name().size),
+                                    event.layer_name().data);
+    event_key = context_->storage->InternString(key_str.string_view());
+  } else {
+    layer_name_id = no_layer_name_name_id_;
+    event_key = context_->storage->InternString(
+        base::StackString<1024>("%u", event.buffer_id()).string_view());
+  }
+
+  CreateBufferEvent(timestamp, event, layer_name_id, event_key);
+  CreatePhaseEvent(timestamp, event, layer_name_id, event_key);
+}
+
+void GraphicsFrameEventParser::CreateBufferEvent(
+    int64_t timestamp,
+    const GraphicsFrameEventDecoder& event,
+    StringId layer_name_id,
+    StringId event_key) {
+  auto* it = buffer_event_map_.Insert(event_key, {}).first;
+  switch (event.type()) {
+    case GraphicsFrameEvent::DEQUEUE:
+      break;
+    case GraphicsFrameEvent::ACQUIRE_FENCE:
+      it->acquire_ts = timestamp;
+      break;
+    case GraphicsFrameEvent::QUEUE:
+      it->queue_ts = timestamp;
+      break;
+    case GraphicsFrameEvent::LATCH:
+      it->latch_ts = timestamp;
+      break;
+    default:
+      context_->storage->IncrementStats(
+          stats::graphics_frame_event_parser_errors);
+      PERFETTO_ELOG("GraphicsFrameEvent with unknown type %d.", event.type());
+      break;
+  }
+  bool prev_is_dequeue = it->is_most_recent_dequeue_;
+  it->is_most_recent_dequeue_ =
+      event.type() ==
+      protos::pbzero::GraphicsFrameEvent::BufferEventType::DEQUEUE;
+
+  StringId event_name_id;
+  if (event.has_type() &&
+      static_cast<uint32_t>(event.type()) < event_type_name_ids_.size()) {
+    event_name_id = event_type_name_ids_[static_cast<uint32_t>(event.type())];
+  } else {
+    event_name_id = unknown_event_name_id_;
+  }
+
+  base::StackString<4096> track_name("Buffer: %u %.*s", event.buffer_id(),
+                                     int(event.layer_name().size),
+                                     event.layer_name().data);
+  TrackId track_id = context_->track_tracker->InternTrack(
+      kGraphicFrameEventBlueprint, tracks::Dimensions(track_name.string_view()),
+      tracks::DynamicName(
+          context_->storage->InternString(track_name.string_view())));
+
+  // Update the frame number for the previous dequeue event.
+  uint32_t frame_number = event.has_frame_number() ? event.frame_number() : 0;
+  if (event.type() == GraphicsFrameEvent::QUEUE && prev_is_dequeue) {
+    context_->slice_tracker->AddArgs(
+        track_id, kNullStringId, kNullStringId,
+        [&](ArgsTracker::BoundInserter* inserter) {
+          inserter->AddArg(frame_number_id_, Variadic::Integer(frame_number));
+        });
+  }
+
+  const int64_t duration =
+      event.has_duration_ns() ? static_cast<int64_t>(event.duration_ns()) : 0;
+  context_->slice_tracker->Scoped(
+      timestamp, track_id, kNullStringId, event_name_id, duration,
+      [&](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(frame_number_id_, Variadic::Integer(frame_number));
+        inserter->AddArg(layer_name_key_id_, Variadic::String(layer_name_id));
+        inserter->AddArg(
+            queue_to_acquire_time_id_,
+            Variadic::Integer(std::max(it->acquire_ts - it->queue_ts,
+                                       static_cast<int64_t>(0))));
+        inserter->AddArg(acquire_to_latch_time_id_,
+                         Variadic::Integer(it->latch_ts - it->acquire_ts));
+        inserter->AddArg(latch_to_present_time_id_,
+                         Variadic::Integer(timestamp - it->latch_ts));
+      });
+}
+
+// Here we convert the buffer events into Phases(slices)
+// APP: Dequeue to Queue
+// Wait for GPU: Queue to Acquire
+// SurfaceFlinger (SF): Latch to Present
+// Display: Present to next Present (of the same layer)
+void GraphicsFrameEventParser::CreatePhaseEvent(
+    int64_t timestamp,
+    const GraphicsFrameEventDecoder& event,
+    StringId layer_name_id,
+    StringId event_key) {
+  auto* slices = context_->storage->mutable_slice_table();
+  auto [it, inserted] = phase_event_map_.Insert(event_key, {});
+  switch (event.type()) {
+    case GraphicsFrameEvent::DEQUEUE: {
+      if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+        // Error handling
+        auto rr = d->slice_row.ToRowReference(slices);
+        rr.set_name(context_->storage->InternString("0"));
+        context_->slice_tracker->AddArgs(
+            rr.track_id(), kNullStringId, kNullStringId,
+            [&](ArgsTracker::BoundInserter* inserter) {
+              inserter->AddArg(frame_number_id_, Variadic::Integer(0));
+            });
+        it->most_recent_event = std::monostate();
+      }
+
+      base::StackString<1024> track_name("APP_%u %.*s", event.buffer_id(),
+                                         int(event.layer_name().size),
+                                         event.layer_name().data);
+      TrackId track_id = context_->track_tracker->InternTrack(
+          kGraphicFrameEventBlueprint,
+          tracks::Dimensions(track_name.string_view()),
+          tracks::DynamicName(
+              context_->storage->InternString(track_name.string_view())));
+      auto res = InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+      if (res) {
+        it->most_recent_event = DequeueInfo{*res, timestamp};
+      }
+      break;
+    }
+    case GraphicsFrameEvent::QUEUE: {
+      if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+        auto slice_rr = d->slice_row.ToRowReference(slices);
+        context_->slice_tracker->End(
+            timestamp, slice_rr.track_id(), kNullStringId, kNullStringId,
+            [&](ArgsTracker::BoundInserter* inserter) {
+              inserter->AddArg(frame_number_id_,
+                               Variadic::Integer(event.frame_number()));
+            });
+
+        // Set the name of the slice to be the frame number since dequeue did
+        // not have a frame number at that time.
+        slice_rr.set_name(context_->storage->InternString(
+            std::to_string(event.frame_number())));
+
+        // The AcquireFence might be signaled before receiving a QUEUE event
+        // sometimes. In that case, we shouldn't start a slice.
+        if (it->last_acquire_ts && *it->last_acquire_ts > d->timestamp) {
+          it->most_recent_event = std::monostate();
+          return;
+        }
+      }
+      base::StackString<1024> track_name("GPU_%u %.*s", event.buffer_id(),
+                                         int(event.layer_name().size),
+                                         event.layer_name().data);
+      StringId track_name_id =
+          context_->storage->InternString(track_name.string_view());
+      TrackId track_id = context_->track_tracker->InternTrack(
+          kGraphicFrameEventBlueprint,
+          tracks::Dimensions(track_name.string_view()),
+          tracks::DynamicName(track_name_id));
+      InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+      it->most_recent_event = QueueInfo{track_id};
+      break;
+    }
+    case GraphicsFrameEvent::ACQUIRE_FENCE: {
+      if (auto* q = std::get_if<QueueInfo>(&it->most_recent_event)) {
+        context_->slice_tracker->End(timestamp, q->track);
+        it->most_recent_event = std::monostate();
+      }
+      it->last_acquire_ts = timestamp;
+      break;
+    }
+    case GraphicsFrameEvent::LATCH: {
+      // b/157578286 - Sometimes Queue event goes missing. To prevent having a
+      // wrong slice info, we try to close any existing APP slice.
+      if (auto* d = std::get_if<DequeueInfo>(&it->most_recent_event)) {
+        auto rr = d->slice_row.ToRowReference(slices);
+        rr.set_name(context_->storage->InternString("0"));
+        context_->slice_tracker->AddArgs(
+            rr.track_id(), kNullStringId, kNullStringId,
+            [&](ArgsTracker::BoundInserter* inserter) {
+              inserter->AddArg(frame_number_id_, Variadic::Integer(0));
+            });
+      }
+      base::StackString<1024> track_name("SF_%u %.*s", event.buffer_id(),
+                                         int(event.layer_name().size),
+                                         event.layer_name().data);
+      TrackId track_id = context_->track_tracker->InternTrack(
+          kGraphicFrameEventBlueprint,
+          tracks::Dimensions(track_name.string_view()),
+          tracks::DynamicName(
+              context_->storage->InternString(track_name.string_view())));
+      InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+      it->most_recent_event = LatchInfo{track_id};
+      break;
+    }
+    case GraphicsFrameEvent::PRESENT_FENCE: {
+      if (auto* l = std::get_if<LatchInfo>(&it->most_recent_event)) {
+        context_->slice_tracker->End(timestamp, l->track);
+        it->most_recent_event = std::monostate();
+      }
+      auto [d_it, d_inserted] = display_map_.Insert(layer_name_id, {});
+      if (d_it) {
+        context_->slice_tracker->End(timestamp, *d_it);
+      }
+      base::StackString<1024> track_name("Display_%.*s",
+                                         int(event.layer_name().size),
+                                         event.layer_name().data);
+      TrackId track_id = context_->track_tracker->InternTrack(
+          kGraphicFrameEventBlueprint,
+          tracks::Dimensions(track_name.string_view()),
+          tracks::DynamicName(
+              context_->storage->InternString(track_name.string_view())));
+      InsertPhaseSlice(timestamp, event, track_id, layer_name_id);
+      *d_it = track_id;
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+std::optional<GraphicsFrameEventParser::SliceRowNumber>
+GraphicsFrameEventParser::InsertPhaseSlice(
+    int64_t timestamp,
+    const GraphicsFrameEventDecoder& event,
+    TrackId track_id,
+    StringId layer_name_id) {
+  // If the frame_number is known, set it as the name of the slice.
+  // If not known (DEQUEUE), set the name as the timestamp.
+  // Timestamp is chosen here because the stack_id is hashed based on the name
+  // of the slice. To not have any conflicting stack_id with any of the
+  // existing slices, we use timestamp as the temporary name.
+  StringId slice_name;
+  if (event.frame_number() != 0) {
+    slice_name =
+        context_->storage->InternString(std::to_string(event.frame_number()));
+  } else {
+    slice_name = context_->storage->InternString(std::to_string(timestamp));
+  }
+  auto slice_id = context_->slice_tracker->Begin(
+      timestamp, track_id, kNullStringId, slice_name,
+      [&](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(frame_number_id_,
+                         Variadic::Integer(event.frame_number()));
+        inserter->AddArg(layer_name_key_id_, Variadic::String(layer_name_id));
+      });
+  if (slice_id) {
+    return context_->storage->slice_table().FindById(*slice_id)->ToRowNumber();
+  }
+  return std::nullopt;
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/graphics_frame_event_parser.h b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
index fbf4b0e..563365f 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.h
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
@@ -20,20 +20,16 @@
 #include <array>
 #include <cstdint>
 #include <optional>
-#include <unordered_map>
-#include <vector>
+#include <variant>
 
-#include "perfetto/ext/base/string_writer.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/protozero/field.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/vulkan_memory_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 #include "protos/perfetto/trace/android/graphics_frame_event.pbzero.h"
+#include "src/trace_processor/tables/slice_tables_py.h"
 
-namespace perfetto {
-
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -46,45 +42,68 @@
   void ParseGraphicsFrameEvent(int64_t timestamp, ConstBytes);
 
  private:
+  using SliceRowNumber = tables::SliceTable::RowNumber;
+  struct BufferEvent {
+    int64_t acquire_ts = 0;
+    int64_t queue_ts = 0;
+    int64_t latch_ts = 0;
+    bool is_most_recent_dequeue_ = false;
+  };
+  struct DequeueInfo {
+    tables::SliceTable::RowNumber slice_row;
+    int64_t timestamp;
+  };
+  struct QueueInfo {
+    TrackId track;
+  };
+  struct LatchInfo {
+    TrackId track;
+  };
+  struct PhaseEvent {
+    std::variant<std::monostate, DequeueInfo, QueueInfo, LatchInfo>
+        most_recent_event;
+    std::optional<int64_t> last_acquire_ts;
+  };
+
   using GraphicsFrameEventDecoder =
       protos::pbzero::GraphicsFrameEvent_BufferEvent_Decoder;
   using GraphicsFrameEvent = protos::pbzero::GraphicsFrameEvent;
-  bool CreateBufferEvent(int64_t timestamp, GraphicsFrameEventDecoder& event);
-  void CreatePhaseEvent(int64_t timestamp, GraphicsFrameEventDecoder& event);
-  // Invalidate a phase slice that has one of the events missing
-  void InvalidatePhaseEvent(int64_t timestamp,
-                            TrackId track_id,
-                            bool reset_name = false);
+
+  void CreateBufferEvent(int64_t timestamp,
+                         const GraphicsFrameEventDecoder&,
+                         StringId layer_name_id,
+                         StringId event_key);
+  void CreatePhaseEvent(int64_t timestamp,
+                        const GraphicsFrameEventDecoder&,
+                        StringId layer_name_id,
+                        StringId event_key);
+
+  std::optional<SliceRowNumber> InsertPhaseSlice(
+      int64_t timestamp,
+      const GraphicsFrameEventDecoder&,
+      TrackId track_id,
+      StringId layer_name_id);
 
   TraceProcessorContext* const context_;
   const StringId unknown_event_name_id_;
   const StringId no_layer_name_name_id_;
   const StringId layer_name_key_id_;
-  std::array<StringId, 14> event_type_name_ids_;
   const StringId queue_lost_message_id_;
-  // Map of (buffer ID + layer name) -> slice id of the dequeue event
-  std::unordered_map<StringId, SliceId> dequeue_slice_ids_;
+  const StringId frame_number_id_;
+  const StringId queue_to_acquire_time_id_;
+  const StringId acquire_to_latch_time_id_;
+  const StringId latch_to_present_time_id_;
+  std::array<StringId, 14> event_type_name_ids_;
 
-  // Row indices of frame stats table. Used to populate the slice_id after
-  // inserting the rows.
-  std::vector<uint32_t> graphics_frame_stats_idx_;
-  // Map of (buffer ID + layer name)
-  //    -> (Map of GraphicsFrameEvent -> ts of that event)
-  std::unordered_map<StringId, std::unordered_map<uint64_t, int64_t>>
-      graphics_frame_stats_map_;
+  // Map of (buffer ID + layer name) -> BufferEvent
+  base::FlatHashMap<StringId, BufferEvent> buffer_event_map_;
 
   // Maps of (buffer id + layer name) -> track id
-  std::unordered_map<StringId, TrackId> dequeue_map_;
-  std::unordered_map<StringId, TrackId> queue_map_;
-  std::unordered_map<StringId, TrackId> latch_map_;
-  // Map of layer name -> track id
-  std::unordered_map<StringId, TrackId> display_map_;
+  base::FlatHashMap<StringId, PhaseEvent> phase_event_map_;
 
-  // Maps of (buffer id + layer name) -> timestamp
-  std::unordered_map<StringId, int64_t> last_dequeued_;
-  std::unordered_map<StringId, int64_t> last_acquired_;
+  // Map of layer name -> track id
+  base::FlatHashMap<StringId, TrackId> display_map_;
 };
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_GRAPHICS_FRAME_EVENT_PARSER_H_
diff --git a/src/trace_processor/importers/proto/metadata_module.cc b/src/trace_processor/importers/proto/metadata_module.cc
index 92f5ef6..e9c26ef 100644
--- a/src/trace_processor/importers/proto/metadata_module.cc
+++ b/src/trace_processor/importers/proto/metadata_module.cc
@@ -29,7 +29,6 @@
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/common/tracks.h"
-#include "src/trace_processor/importers/proto/config.descriptor.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/storage/metadata.h"
@@ -69,6 +68,7 @@
   RegisterForField(TracePacket::kUiStateFieldNumber, context);
   RegisterForField(TracePacket::kTriggerFieldNumber, context);
   RegisterForField(TracePacket::kChromeTriggerFieldNumber, context);
+  RegisterForField(TracePacket::kCloneSnapshotTriggerFieldNumber, context);
   RegisterForField(TracePacket::kTraceUuidFieldNumber, context);
 }
 
@@ -117,14 +117,20 @@
   // We handle triggers at parse time rather at tokenization because
   // we add slices to tables which need to happen post-sorting.
   if (field_id == TracePacket::kTriggerFieldNumber) {
-    ParseTrigger(ts, decoder.trigger());
+    ParseTrigger(ts, decoder.trigger(), TraceTriggerPacketType::kTraceTrigger);
   }
   if (field_id == TracePacket::kChromeTriggerFieldNumber) {
     ParseChromeTrigger(ts, decoder.chrome_trigger());
   }
+  if (field_id == TracePacket::kCloneSnapshotTriggerFieldNumber) {
+    ParseTrigger(ts, decoder.clone_snapshot_trigger(),
+                 TraceTriggerPacketType::kCloneSnapshot);
+  }
 }
 
-void MetadataModule::ParseTrigger(int64_t ts, ConstBytes blob) {
+void MetadataModule::ParseTrigger(int64_t ts,
+                                  ConstBytes blob,
+                                  TraceTriggerPacketType packetType) {
   protos::pbzero::Trigger::Decoder trigger(blob.data, blob.size);
   StringId cat_id = kNullStringId;
   TrackId track_id =
@@ -145,6 +151,18 @@
                              Variadic::Integer(trigger.trusted_producer_uid()));
         }
       });
+
+  if (packetType == TraceTriggerPacketType::kCloneSnapshot &&
+      trace_trigger_packet_type_ != TraceTriggerPacketType::kCloneSnapshot) {
+    trace_trigger_packet_type_ = TraceTriggerPacketType::kCloneSnapshot;
+    context_->metadata_tracker->SetMetadata(metadata::trace_trigger,
+                                            Variadic::String(name_id));
+  } else if (packetType == TraceTriggerPacketType::kTraceTrigger &&
+             trace_trigger_packet_type_ == TraceTriggerPacketType::kNone) {
+    trace_trigger_packet_type_ = TraceTriggerPacketType::kTraceTrigger;
+    context_->metadata_tracker->SetMetadata(metadata::trace_trigger,
+                                            Variadic::String(name_id));
+  }
 }
 
 void MetadataModule::ParseChromeTrigger(int64_t ts, ConstBytes blob) {
@@ -218,12 +236,8 @@
                                             Variadic::String(id));
   }
 
-  DescriptorPool pool;
-  pool.AddFromFileDescriptorSet(kConfigDescriptor.data(),
-                                kConfigDescriptor.size());
-
   std::string text = protozero_to_text::ProtozeroToText(
-      pool, ".perfetto.protos.TraceConfig",
+      *context_->descriptor_pool_, ".perfetto.protos.TraceConfig",
       protozero::ConstBytes{
           trace_config.begin(),
           static_cast<uint32_t>(trace_config.end() - trace_config.begin())},
diff --git a/src/trace_processor/importers/proto/metadata_module.h b/src/trace_processor/importers/proto/metadata_module.h
index aabea03..bb6f4f8 100644
--- a/src/trace_processor/importers/proto/metadata_module.h
+++ b/src/trace_processor/importers/proto/metadata_module.h
@@ -47,11 +47,19 @@
   void ParseTraceConfig(const protos::pbzero::TraceConfig_Decoder&) override;
 
  private:
-  void ParseTrigger(int64_t ts, ConstBytes);
+  enum class TraceTriggerPacketType {
+    kNone,
+    kTraceTrigger,
+    kCloneSnapshot,
+  };
+
+  void ParseTrigger(int64_t ts, ConstBytes, TraceTriggerPacketType);
   void ParseChromeTrigger(int64_t ts, ConstBytes);
   void ParseTraceUuid(ConstBytes);
 
   TraceProcessorContext* context_;
+  TraceTriggerPacketType trace_trigger_packet_type_ =
+      TraceTriggerPacketType::kNone;
 
   const StringId producer_name_key_id_;
   const StringId trusted_producer_uid_key_id_;
diff --git a/src/trace_processor/importers/proto/multi_machine_trace_manager.cc b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
index b030eb9..5738910 100644
--- a/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
+++ b/src/trace_processor/importers/proto/multi_machine_trace_manager.cc
@@ -15,31 +15,19 @@
  */
 
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
-#include <memory>
 
-#include "src/trace_processor/importers/common/args_translation_table.h"
-#include "src/trace_processor/importers/common/clock_converter.h"
-#include "src/trace_processor/importers/common/clock_tracker.h"
-#include "src/trace_processor/importers/common/event_tracker.h"
-#include "src/trace_processor/importers/common/flow_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include <memory>
+#include <utility>
+
+#include "perfetto/base/logging.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/slice_tracker.h"
-#include "src/trace_processor/importers/common/stack_profile_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/proto/default_modules.h"
-#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
-#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 MultiMachineTraceManager::MultiMachineTraceManager(
     TraceProcessorContext* default_context)
@@ -93,5 +81,4 @@
   return remote_machine_contexts_[raw_machine_id].reader.get();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
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 64fa9ee..79e9174 100644
--- a/src/trace_processor/importers/proto/network_trace_module_unittest.cc
+++ b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
+#include "src/trace_processor/importers/proto/network_trace_module.h"
+
 #include <cstdint>
 #include <memory>
 #include <vector>
 
-#include "src/trace_processor/importers/proto/network_trace_module.h"
-
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/packed_repeated_fields.h"
@@ -135,15 +135,18 @@
   ASSERT_EQ(slices.row_count(), 1u);
   EXPECT_EQ(slices[0].ts(), 123);
 
-  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(72)));
-  EXPECT_TRUE(HasArg(2u, "socket_uid", Variadic::Integer(1010)));
-  EXPECT_TRUE(HasArg(2u, "local_port", Variadic::Integer(5100)));
-  EXPECT_TRUE(HasArg(2u, "remote_port", Variadic::Integer(443)));
-  EXPECT_TRUE(HasArg(2u, "packet_transport",
+  EXPECT_TRUE(slices[0].arg_set_id().has_value());
+
+  uint32_t arg_set_id = *slices[0].arg_set_id();
+  EXPECT_TRUE(HasArg(arg_set_id, "packet_length", Variadic::Integer(72)));
+  EXPECT_TRUE(HasArg(arg_set_id, "socket_uid", Variadic::Integer(1010)));
+  EXPECT_TRUE(HasArg(arg_set_id, "local_port", Variadic::Integer(5100)));
+  EXPECT_TRUE(HasArg(arg_set_id, "remote_port", Variadic::Integer(443)));
+  EXPECT_TRUE(HasArg(arg_set_id, "packet_transport",
                      Variadic::String(storage_->InternString("IPPROTO_TCP"))));
-  EXPECT_TRUE(HasArg(2u, "socket_tag",
+  EXPECT_TRUE(HasArg(arg_set_id, "socket_tag",
                      Variadic::String(storage_->InternString("0x407"))));
-  EXPECT_TRUE(HasArg(2u, "packet_tcp_flags",
+  EXPECT_TRUE(HasArg(arg_set_id, "packet_tcp_flags",
                      Variadic::String(storage_->InternString(".s..a..."))));
 }
 
@@ -175,8 +178,13 @@
   EXPECT_EQ(slices[0].ts(), 123);
   EXPECT_EQ(slices[1].ts(), 133);
 
-  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(72)));
-  EXPECT_TRUE(HasArg(3u, "packet_length", Variadic::Integer(100)));
+  EXPECT_TRUE(slices[0].arg_set_id().has_value());
+  EXPECT_TRUE(slices[1].arg_set_id().has_value());
+
+  EXPECT_TRUE(
+      HasArg(*slices[0].arg_set_id(), "packet_length", Variadic::Integer(72)));
+  EXPECT_TRUE(
+      HasArg(*slices[1].arg_set_id(), "packet_length", Variadic::Integer(100)));
 }
 
 TEST_F(NetworkTraceModuleTest, TokenizeAndParseAggregateBundle) {
@@ -200,8 +208,11 @@
   EXPECT_EQ(slices[0].ts(), 123);
   EXPECT_EQ(slices[0].dur(), 10);
 
-  EXPECT_TRUE(HasArg(2u, "packet_length", Variadic::Integer(172)));
-  EXPECT_TRUE(HasArg(2u, "packet_count", Variadic::Integer(2)));
+  EXPECT_TRUE(slices[0].arg_set_id().has_value());
+
+  uint32_t arg_set_id = *slices[0].arg_set_id();
+  EXPECT_TRUE(HasArg(arg_set_id, "packet_length", Variadic::Integer(172)));
+  EXPECT_TRUE(HasArg(arg_set_id, "packet_count", Variadic::Integer(2)));
 }
 
 }  // namespace
diff --git a/src/trace_processor/importers/proto/proto_importer_module.h b/src/trace_processor/importers/proto/proto_importer_module.h
index c8375dc..b383072 100644
--- a/src/trace_processor/importers/proto/proto_importer_module.h
+++ b/src/trace_processor/importers/proto/proto_importer_module.h
@@ -54,7 +54,7 @@
 
 class ModuleResult {
  public:
-  // Allow auto conversion from util::Status to Handled / Error result.
+  // Allow auto conversion from base::Status to Handled / Error result.
   ModuleResult(const base::Status& status)
       : ignored_(false),
         error_(status.ok() ? std::nullopt
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
index f1ed9df..5492e19 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -45,6 +45,7 @@
 #include "src/trace_processor/importers/proto/track_event_module.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
@@ -164,10 +165,10 @@
   protos::pbzero::ChromeEventBundle::Decoder bundle(blob);
   ArgsTracker args(context_);
   if (bundle.has_metadata()) {
-    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
-    RawId id = storage->mutable_raw_table()
-                   ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0, 0, ucpu})
-                   .id;
+    tables::ChromeRawTable::Id id =
+        storage->mutable_chrome_raw_table()
+            ->Insert({ts, raw_chrome_metadata_event_id_, 0, 0})
+            .id;
     auto inserter = args.AddArgsTo(id);
 
     uint32_t bundle_index =
@@ -212,11 +213,10 @@
   }
 
   if (bundle.has_legacy_ftrace_output()) {
-    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
-    RawId id = storage->mutable_raw_table()
-                   ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0,
-                             0, ucpu})
-                   .id;
+    tables::ChromeRawTable::Id id =
+        storage->mutable_chrome_raw_table()
+            ->Insert({ts, raw_chrome_legacy_system_trace_event_id_, 0, 0})
+            .id;
 
     std::string data;
     for (auto it = bundle.legacy_ftrace_output(); it; ++it) {
@@ -234,11 +234,10 @@
           protos::pbzero::ChromeLegacyJsonTrace::USER_TRACE) {
         continue;
       }
-      auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
-      RawId id = storage->mutable_raw_table()
-                     ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0,
-                               0, ucpu})
-                     .id;
+      tables::ChromeRawTable::Id id =
+          storage->mutable_chrome_raw_table()
+              ->Insert({ts, raw_chrome_legacy_user_trace_event_id_, 0, 0})
+              .id;
       Variadic value =
           Variadic::String(storage->InternString(legacy_trace.data()));
       args.AddArgsTo(id).AddArg(data_name_id_, value);
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
index b0a5355..190f31f 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
@@ -19,14 +19,12 @@
 #include <cmath>
 #include <cstdint>
 #include <cstring>
-#include <functional>
 #include <memory>
 #include <optional>
 #include <string>
 #include <utility>
 #include <vector>
 
-#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
@@ -55,6 +53,7 @@
 #include "src/trace_processor/importers/proto/additional_modules.h"
 #include "src/trace_processor/importers/proto/default_modules.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
+#include "src/trace_processor/importers/proto/trace.descriptor.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
@@ -233,45 +232,6 @@
   ArgsTracker tracker_;
 };
 
-class MockSliceTracker : public SliceTracker {
- public:
-  explicit MockSliceTracker(TraceProcessorContext* context)
-      : SliceTracker(context) {}
-
-  MOCK_METHOD(std::optional<SliceId>,
-              Begin,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              End,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              Scoped,
-              (int64_t timestamp,
-               TrackId track_id,
-               StringId cat,
-               StringId name,
-               int64_t duration,
-               SetArgsCallback args_callback),
-              (override));
-  MOCK_METHOD(std::optional<SliceId>,
-              StartSlice,
-              (int64_t timestamp,
-               TrackId track_id,
-               SetArgsCallback args_callback,
-               std::function<SliceId()> inserter),
-              (override));
-};
-
 class ProtoTraceParserTest : public ::testing::Test {
  public:
   ProtoTraceParserTest() {
@@ -297,8 +257,7 @@
     context_.process_tracker.reset(process_);
     context_.process_track_translation_table.reset(
         new ProcessTrackTranslationTable(storage_));
-    slice_ = new NiceMock<MockSliceTracker>(&context_);
-    context_.slice_tracker.reset(slice_);
+    context_.slice_tracker = std::make_unique<SliceTracker>(&context_);
     context_.slice_translation_table =
         std::make_unique<SliceTranslationTable>(storage_);
     clock_ = new ClockTracker(&context_);
@@ -309,6 +268,8 @@
     context_.sorter = std::make_shared<TraceSorter>(
         &context_, TraceSorter::SortingMode::kFullSort);
     context_.descriptor_pool_ = std::make_unique<DescriptorPool>();
+    context_.descriptor_pool_->AddFromFileDescriptorSet(
+        kTraceDescriptor.data(), kTraceDescriptor.size());
 
     context_.perf_sample_tracker.reset(new PerfSampleTracker(&context_));
 
@@ -358,7 +319,6 @@
   MockEventTracker* event_;
   MockSchedEventTracker* sched_;
   MockProcessTracker* process_;
-  MockSliceTracker* slice_;
   ClockTracker* clock_;
   TraceStorage* storage_;
 };
@@ -391,7 +351,7 @@
   context_.sorter->ExtractEventsForced();
 }
 
-TEST_F(ProtoTraceParserTest, LoadEventsIntoRaw) {
+TEST_F(ProtoTraceParserTest, LoadEventsIntoFtraceEvent) {
   auto* bundle = trace_->add_packet()->set_ftrace_events();
   bundle->set_cpu(10);
 
@@ -422,7 +382,7 @@
   Tokenize();
   context_.sorter->ExtractEventsForced();
 
-  const auto& raw = context_.storage->raw_table();
+  const auto& raw = context_.storage->ftrace_event_table();
   ASSERT_EQ(raw.row_count(), 2u);
   const auto& args = context_.storage->arg_table();
   ASSERT_EQ(args.row_count(), 6u);
@@ -475,7 +435,7 @@
   Tokenize();
   context_.sorter->ExtractEventsForced();
 
-  const auto& raw = storage_->raw_table();
+  const auto& raw = storage_->ftrace_event_table();
 
   ASSERT_EQ(raw.row_count(), 1u);
   ASSERT_EQ(raw[raw.row_count() - 1].ts(), 100);
@@ -978,28 +938,16 @@
 
   MockBoundInserter inserter;
 
-  StringId unknown_cat = storage_->InternString("unknown(1)");
-
-  constexpr TrackId track{0u};
   constexpr TrackId thread_time_track{1u};
 
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
   // Only the begin thread time can be imported into the counter table.
   EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(2003000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1005000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(1u))));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, End(1020000, track, unknown_cat, kNullStringId, _))
-      .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
-
   context_.sorter->ExtractEventsForced();
 
   EXPECT_EQ(storage_->slice_table().row_count(), 2u);
@@ -1068,26 +1016,15 @@
 
   MockBoundInserter inserter;
 
-  StringId unknown_cat1 = storage_->InternString("unknown(1)");
-
-  constexpr TrackId track{0u};
   constexpr TrackId thread_time_track{1u};
 
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
   EXPECT_CALL(*event_, PushCounter(1015000, testing::DoubleEq(2007000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, StartSlice(1015000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(1u))));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
                                    thread_time_track));
-  EXPECT_CALL(*slice_, End(1020000, track, unknown_cat1, kNullStringId, _))
-      .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(0u))));
 
   context_.sorter->ExtractEventsForced();
 
@@ -1240,13 +1177,8 @@
   row.upid = 2u;
   storage_->mutable_thread_table()->Insert(row);
 
-  constexpr TrackId thread_1_track{0u};
   constexpr TrackId thread_time_track{1u};
   constexpr TrackId thread_instruction_count_track{2u};
-  constexpr TrackId process_2_track{3u};
-
-  StringId cat_1 = storage_->InternString("cat1");
-  StringId ev_1 = storage_->InternString("ev1");
 
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
 
@@ -1256,41 +1188,25 @@
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1005000, testing::DoubleEq(3010),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, StartSlice(1005000, thread_1_track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
 
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(2005000),
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(3020),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, StartSlice(1010000, thread_1_track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(1u))));
 
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(3040),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, End(1020000, thread_1_track, cat_1, ev_1, _))
-      .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(1u))));
 
   EXPECT_CALL(*event_, PushCounter(1040000, testing::DoubleEq(2030000),
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1040000, testing::DoubleEq(3100),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, StartSlice(1040000, thread_1_track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(2u))));
-
-  EXPECT_CALL(*slice_, Scoped(1050000, process_2_track, cat_1, ev_1, 0, _))
-      .WillOnce(DoAll(InvokeArgument<5>(&inserter), Return(SliceId(3u))));
-  // Second slice should have a legacy_event.passthrough_utid arg.
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::UnsignedInteger(1u), _));
 
   context_.sorter->ExtractEventsForced();
 
-  EXPECT_EQ(storage_->slice_table().row_count(), 3u);
+  EXPECT_EQ(storage_->slice_table().row_count(), 4u);
   auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
   EXPECT_TRUE(rr_0);
   EXPECT_EQ(rr_0->thread_ts(), 2003000);
@@ -1413,9 +1329,7 @@
   row.upid = 1u;
   storage_->mutable_thread_table()->Insert(row);
 
-  StringId cat_1 = storage_->InternString("cat1");
   StringId ev_1 = storage_->InternString("ev1");
-  StringId cat_2 = storage_->InternString("cat2");
   StringId ev_2 = storage_->InternString("ev2");
 
   TrackId thread_time_track{2u};
@@ -1427,17 +1341,10 @@
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1010000, testing::DoubleEq(3020),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, Begin(1010000, TrackId{1}, cat_1, ev_1, _))
-      .WillOnce(Return(SliceId(0u)));
-  EXPECT_CALL(*slice_, Scoped(1015000, TrackId{1}, cat_1, ev_2, 0, _));
-  EXPECT_CALL(*slice_, Scoped(1018000, TrackId{4}, cat_2, ev_2, 0, _));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(2010000),
                                    thread_time_track));
   EXPECT_CALL(*event_, PushCounter(1020000, testing::DoubleEq(3040),
                                    thread_instruction_count_track));
-  EXPECT_CALL(*slice_, End(1020000, TrackId{1}, cat_1, ev_1, _))
-      .WillOnce(Return(SliceId(SliceId(0u))));
-  EXPECT_CALL(*slice_, Scoped(1030000, TrackId{5}, cat_2, ev_2, 0, _));
 
   context_.sorter->ExtractEventsForced();
 
@@ -1602,27 +1509,13 @@
 
   Tokenize();
 
-  StringId cat_1 = storage_->InternString("cat1");
-  StringId ev_1 = storage_->InternString("ev1");
-
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
 
-  EXPECT_CALL(*slice_, Begin(1010000, TrackId{1}, cat_1, ev_1, _))
-      .WillOnce(Return(SliceId(2u)));
-
   EXPECT_CALL(*event_,
               PushCounter(1015000, testing::DoubleEq(2007000), TrackId{3}));
-  EXPECT_CALL(*slice_, StartSlice(1015000, TrackId{0}, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(0u))));
 
   EXPECT_CALL(*event_,
               PushCounter(1016000, testing::DoubleEq(2008000), TrackId{4}));
-  EXPECT_CALL(*slice_, StartSlice(1016000, TrackId{2}, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(1u))));
-
-  EXPECT_CALL(*slice_,
-              End(1020000, TrackId{1}, kNullStringId, kNullStringId, _))
-      .WillOnce(Return(SliceId(2u)));
 
   context_.sorter->ExtractEventsForced();
 
@@ -1640,7 +1533,7 @@
   EXPECT_EQ(storage_->track_table()[4].utid(), 2u);
 
   EXPECT_EQ(storage_->virtual_track_slices().slice_count(), 1u);
-  EXPECT_EQ(storage_->virtual_track_slices().slice_ids()[0], SliceId(2u));
+  EXPECT_EQ(storage_->virtual_track_slices().slice_ids()[0], SliceId(0u));
   EXPECT_EQ(storage_->virtual_track_slices().thread_timestamp_ns()[0], 2005000);
   EXPECT_EQ(storage_->virtual_track_slices().thread_duration_ns()[0], 5000);
   EXPECT_EQ(storage_->virtual_track_slices().thread_instruction_counts()[0],
@@ -1648,15 +1541,15 @@
   EXPECT_EQ(storage_->virtual_track_slices().thread_instruction_deltas()[0],
             20);
 
-  EXPECT_EQ(storage_->slice_table().row_count(), 2u);
-  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_EQ(storage_->slice_table().row_count(), 3u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(1u));
   EXPECT_TRUE(rr_0);
   EXPECT_EQ(rr_0->thread_ts(), 2007000);
   EXPECT_EQ(rr_0->thread_dur(), 0);
   // There was no thread instructions in the packets above.
   EXPECT_FALSE(rr_0->thread_instruction_count());
   EXPECT_FALSE(rr_0->thread_instruction_delta());
-  auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+  auto rr_1 = storage_->slice_table().FindById(SliceId(2u));
   EXPECT_TRUE(rr_1);
   EXPECT_EQ(rr_1->thread_ts(), 2008000);
   EXPECT_EQ(rr_1->thread_dur(), 0);
@@ -1737,13 +1630,9 @@
 
   EXPECT_CALL(*event_,
               PushCounter(1000, testing::DoubleEq(1000000), TrackId{1}));
-  EXPECT_CALL(*slice_, StartSlice(1000, TrackId{0}, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()), Return(SliceId(0u))));
 
   EXPECT_CALL(*event_,
               PushCounter(1100, testing::DoubleEq(1010000), TrackId{1}));
-  EXPECT_CALL(*slice_, End(1100, TrackId{0}, kNullStringId, kNullStringId, _))
-      .WillOnce(Return(SliceId(0u)));
 
   EXPECT_CALL(*process_,
               UpdateThreadNameByUtid(1u, storage_->InternString("t1"),
@@ -1816,9 +1705,13 @@
   StringId cat1 = storage_->InternString("cat1");
   StringId ev2 = storage_->InternString("ev2");
 
-  EXPECT_CALL(*slice_, Scoped(2100000, TrackId{0}, cat1, ev2, 0, _))
-      .WillOnce(Return(SliceId(0u)));
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->category(), cat1);
+  EXPECT_EQ(rr_0->name(), ev2);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventWithoutThreadDescriptor) {
@@ -1855,9 +1748,16 @@
   StringId cat1 = storage_->InternString("cat1");
   StringId ev1 = storage_->InternString("ev1");
 
-  EXPECT_CALL(*slice_, Scoped(2000000, TrackId{0}, cat1, ev1, 0, _))
-      .WillOnce(Return(SliceId(0u)));
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 2000000);
+  EXPECT_EQ(rr_0->track_id(), TrackId{0});
+  EXPECT_EQ(rr_0->dur(), 0);
+  EXPECT_EQ(rr_0->category(), cat1);
+  EXPECT_EQ(rr_0->name(), ev1);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventWithDataLoss) {
@@ -1945,10 +1845,17 @@
   StringId unknown_cat = storage_->InternString("unknown(1)");
   constexpr TrackId track{0u};
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _));
-  EXPECT_CALL(*slice_, End(2010000, track, unknown_cat, kNullStringId, _));
 
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1010000);
+  EXPECT_EQ(rr_0->track_id(), track);
+  EXPECT_EQ(rr_0->dur(), 1000000);
+  EXPECT_EQ(rr_0->category(), unknown_cat);
+  EXPECT_EQ(rr_0->name(), std::nullopt);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventMultipleSequences) {
@@ -2048,12 +1955,24 @@
   constexpr TrackId thread_1_track{1u};
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
 
-  EXPECT_CALL(*slice_, StartSlice(1005000, thread_2_track, _, _));
-  EXPECT_CALL(*slice_, StartSlice(1010000, thread_1_track, _, _));
-  EXPECT_CALL(*slice_, End(1015000, thread_2_track, cat_1, ev_2, _));
-  EXPECT_CALL(*slice_, End(1020000, thread_1_track, cat_1, ev_1, _));
-
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 2u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1005000);
+  EXPECT_EQ(rr_0->track_id(), thread_2_track);
+  EXPECT_EQ(rr_0->dur(), 10000);
+  EXPECT_EQ(rr_0->category(), cat_1);
+  EXPECT_EQ(rr_0->name(), ev_2);
+
+  auto rr_1 = storage_->slice_table().FindById(SliceId(1u));
+  EXPECT_TRUE(rr_1);
+  EXPECT_EQ(rr_1->ts(), 1010000);
+  EXPECT_EQ(rr_1->track_id(), thread_1_track);
+  EXPECT_EQ(rr_1->dur(), 10000);
+  EXPECT_EQ(rr_1->category(), cat_1);
+  EXPECT_EQ(rr_1->name(), ev_1);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventWithDebugAnnotations) {
@@ -2186,75 +2105,19 @@
 
   StringId cat_1 = storage_->InternString("cat1");
   StringId ev_1 = storage_->InternString("ev1");
-  StringId debug_an_1 = storage_->InternString("debug.an1");
-  StringId debug_an_2_child_1 = storage_->InternString("debug.an2.child1");
-  StringId debug_an_2_child_2 = storage_->InternString("debug.an2.child2");
-  StringId debug_an_2_child_2_0 = storage_->InternString("debug.an2.child2[0]");
-  StringId child21 = storage_->InternString("child21");
-  StringId debug_an_2_child_2_1 = storage_->InternString("debug.an2.child2[1]");
-  StringId debug_an_2_child_2_2 = storage_->InternString("debug.an2.child2[2]");
-  StringId debug_an_3 = storage_->InternString("debug.an3");
-  StringId debug_an_4 = storage_->InternString("debug.an4");
-  StringId debug_an_5 = storage_->InternString("debug.an5");
-  StringId debug_an_6 = storage_->InternString("debug.an6");
-  StringId debug_an_7 = storage_->InternString("debug.an7");
-  StringId val_7 = storage_->InternString("val7");
-  StringId debug_an_8_val8_a = storage_->InternString("debug.an8.val8.a");
-  StringId debug_an_8_val8_b = storage_->InternString("debug.an8.val8.b");
-  StringId val_8b = storage_->InternString("val8b");
-  StringId debug_an_8_arr8 = storage_->InternString("debug.an8.arr8");
-  StringId debug_an_8_arr8_0 = storage_->InternString("debug.an8.arr8[0]");
-  StringId debug_an_8_arr8_1 = storage_->InternString("debug.an8.arr8[1]");
-  StringId debug_an_8_arr8_2 = storage_->InternString("debug.an8.arr8[2]");
-  StringId debug_an_8_foo = storage_->InternString("debug.an8_foo");
 
   constexpr TrackId track{0u};
 
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
-  EXPECT_CALL(inserter, AddArg(debug_an_1, debug_an_1,
-                               Variadic::UnsignedInteger(10u), _));
-
-  EXPECT_CALL(inserter, AddArg(debug_an_2_child_1, debug_an_2_child_1,
-                               Variadic::Boolean(true), _));
-
-  EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_0,
-                               Variadic::String(child21), _));
-
-  EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_1,
-                               Variadic::Real(2.2), _));
-
-  EXPECT_CALL(inserter, AddArg(debug_an_2_child_2, debug_an_2_child_2_2,
-                               Variadic::Integer(23), _));
-
-  EXPECT_CALL(*slice_, End(1020000, track, cat_1, ev_1, _))
-      .WillOnce(DoAll(InvokeArgument<4>(&inserter), Return(SliceId(0u))));
-
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_3, debug_an_3, Variadic::Integer(-3), _));
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_4, debug_an_4, Variadic::Boolean(true), _));
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_5, debug_an_5, Variadic::Real(-5.5), _));
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_6, debug_an_6, Variadic::Pointer(20u), _));
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_7, debug_an_7, Variadic::String(val_7), _));
-  EXPECT_CALL(inserter, AddArg(debug_an_8_val8_a, debug_an_8_val8_a,
-                               Variadic::Integer(42), _));
-  EXPECT_CALL(inserter, AddArg(debug_an_8_val8_b, debug_an_8_val8_b,
-                               Variadic::String(val_8b), _));
-  EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_0,
-                               Variadic::Integer(1), _));
-  EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_1,
-                               Variadic::Integer(2), _));
-  EXPECT_CALL(inserter, AddArg(debug_an_8_arr8, debug_an_8_arr8_2,
-                               Variadic::Integer(3), _));
-  EXPECT_CALL(inserter,
-              AddArg(debug_an_8_foo, debug_an_8_foo, Variadic::Integer(15), _));
-
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1010000);
+  EXPECT_EQ(rr_0->track_id(), track);
+  EXPECT_EQ(rr_0->dur(), 10000);
+  EXPECT_EQ(rr_0->category(), cat_1);
+  EXPECT_EQ(rr_0->name(), ev_1);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventWithTaskExecution) {
@@ -2303,20 +2166,15 @@
 
   constexpr TrackId track{0u};
 
-  StringId file_1 = storage_->InternString("file1");
-  StringId func_1 = storage_->InternString("func1");
-
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
 
-  MockBoundInserter inserter;
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(file_1), _));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(func_1), _));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::UnsignedInteger(42), _));
-
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1010000);
+  EXPECT_EQ(rr_0->track_id(), track);
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventWithLogMessage) {
@@ -2373,26 +2231,19 @@
   storage_->mutable_thread_table()->Insert(row);
 
   StringId body_1 = storage_->InternString("body1");
-  StringId file_1 = storage_->InternString("file1");
-  StringId func_1 = storage_->InternString("func1");
   StringId source_location_id = storage_->InternString("file1:1");
 
   constexpr TrackId track{0};
   InSequence in_sequence;  // Below slices should be sorted by timestamp.
 
-  MockBoundInserter inserter;
-  EXPECT_CALL(*slice_, StartSlice(1010000, track, _, _))
-      .WillOnce(DoAll(IgnoreResult(InvokeArgument<3>()),
-                      InvokeArgument<2>(&inserter), Return(SliceId(0u))));
-
-  // Call with logMessageBody (body1 in this case).
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(body_1), _));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(file_1), _));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::String(func_1), _));
-  EXPECT_CALL(inserter, AddArg(_, _, Variadic::Integer(1), _));
-
   context_.sorter->ExtractEventsForced();
 
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 1010000);
+  EXPECT_EQ(rr_0->track_id(), track);
+
   EXPECT_GT(context_.storage->android_log_table().row_count(), 0u);
   EXPECT_EQ(context_.storage->android_log_table()[0].ts(), 1010000);
   EXPECT_EQ(context_.storage->android_log_table()[0].msg(), body_1);
@@ -2466,38 +2317,41 @@
   ::testing::Mock::VerifyAndClearExpectations(storage_);
 
   // Verify raw_table and args contents.
-  const auto& raw_table = storage_->raw_table();
+  const auto& raw_table = storage_->chrome_raw_table();
   EXPECT_EQ(raw_table.row_count(), 1u);
   EXPECT_EQ(raw_table[0].ts(), 1010000);
   EXPECT_EQ(raw_table[0].name(),
             storage_->InternString("track_event.legacy_event"));
-  auto ucpu = raw_table[0].ucpu();
-  const auto& cpu_table = storage_->cpu_table();
-  EXPECT_EQ(cpu_table[ucpu.value].cpu(), 0u);
   EXPECT_EQ(raw_table[0].utid(), 1u);
-  EXPECT_EQ(raw_table[0].arg_set_id(), 2u);
+  EXPECT_TRUE(raw_table[0].arg_set_id());
 
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.category"),
+  uint32_t arg_set_id = raw_table[0].arg_set_id();
+  EXPECT_TRUE(HasArg(arg_set_id,
+                     storage_->InternString("legacy_event.category"),
                      Variadic::String(cat_1)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.name"),
+  EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("legacy_event.name"),
                      Variadic::String(ev_1)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.phase"),
+  EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("legacy_event.phase"),
                      Variadic::String(question)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.duration_ns"),
+  EXPECT_TRUE(HasArg(arg_set_id,
+                     storage_->InternString("legacy_event.duration_ns"),
                      Variadic::Integer(23000)));
-  EXPECT_TRUE(HasArg(2u,
+  EXPECT_TRUE(HasArg(arg_set_id,
                      storage_->InternString("legacy_event.thread_timestamp_ns"),
                      Variadic::Integer(2005000)));
-  EXPECT_TRUE(HasArg(2u,
+  EXPECT_TRUE(HasArg(arg_set_id,
                      storage_->InternString("legacy_event.thread_duration_ns"),
                      Variadic::Integer(15000)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.use_async_tts"),
+  EXPECT_TRUE(HasArg(arg_set_id,
+                     storage_->InternString("legacy_event.use_async_tts"),
                      Variadic::Boolean(true)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.global_id"),
+  EXPECT_TRUE(HasArg(arg_set_id,
+                     storage_->InternString("legacy_event.global_id"),
                      Variadic::UnsignedInteger(99u)));
-  EXPECT_TRUE(HasArg(2u, storage_->InternString("legacy_event.id_scope"),
+  EXPECT_TRUE(HasArg(arg_set_id,
+                     storage_->InternString("legacy_event.id_scope"),
                      Variadic::String(scope_1)));
-  EXPECT_TRUE(HasArg(2u, debug_an_1, Variadic::UnsignedInteger(10u)));
+  EXPECT_TRUE(HasArg(arg_set_id, debug_an_1, Variadic::UnsignedInteger(10u)));
 }
 
 TEST_F(ProtoTraceParserTest, TrackEventLegacyTimestampsWithClockSnapshot) {
@@ -2533,12 +2387,14 @@
   storage_->mutable_thread_table()->Insert(row);
 
   constexpr TrackId track{0u};
-  InSequence in_sequence;  // Below slices should be sorted by timestamp.
-
-  // Timestamp should be adjusted to trace time (BOOTTIME).
-  EXPECT_CALL(*slice_, StartSlice(10000, track, _, _));
 
   context_.sorter->ExtractEventsForced();
+
+  EXPECT_EQ(storage_->slice_table().row_count(), 1u);
+  auto rr_0 = storage_->slice_table().FindById(SliceId(0u));
+  EXPECT_TRUE(rr_0);
+  EXPECT_EQ(rr_0->ts(), 10000);
+  EXPECT_EQ(rr_0->track_id(), track);
 }
 
 TEST_F(ProtoTraceParserTest, ParseEventWithClockIdButWithoutClockSnapshot) {
@@ -2558,7 +2414,7 @@
   context_.sorter->ExtractEventsForced();
 
   // Metadata should have created a raw event.
-  const auto& raw_table = storage_->raw_table();
+  const auto& raw_table = storage_->chrome_raw_table();
   EXPECT_EQ(raw_table.row_count(), 1u);
 }
 
@@ -2586,16 +2442,16 @@
   context_.sorter->ExtractEventsForced();
 
   // Verify raw_table and args contents.
-  const auto& raw_table = storage_->raw_table();
+  const auto& raw_table = storage_->chrome_raw_table();
   EXPECT_EQ(raw_table.row_count(), 1u);
   EXPECT_EQ(raw_table[0].name(),
             storage_->InternString("chrome_event.metadata"));
-  EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
 
+  uint32_t arg_set_id = raw_table[0].arg_set_id();
   EXPECT_EQ(storage_->arg_table().row_count(), 2u);
-  EXPECT_TRUE(HasArg(1u, storage_->InternString(kStringName),
+  EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString(kStringName),
                      Variadic::String(storage_->InternString(kStringValue))));
-  EXPECT_TRUE(HasArg(1u, storage_->InternString(kIntName),
+  EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString(kIntName),
                      Variadic::Integer(kIntValue)));
 }
 
@@ -2617,14 +2473,14 @@
   context_.sorter->ExtractEventsForced();
 
   // Verify raw_table and args contents.
-  const auto& raw_table = storage_->raw_table();
+  const auto& raw_table = storage_->chrome_raw_table();
   EXPECT_EQ(raw_table.row_count(), 1u);
   EXPECT_EQ(raw_table[0].name(),
             storage_->InternString("chrome_event.legacy_system_trace"));
-  EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
 
   EXPECT_EQ(storage_->arg_table().row_count(), 1u);
-  EXPECT_TRUE(HasArg(1u, storage_->InternString("data"),
+  uint32_t arg_set_id = raw_table[0].arg_set_id();
+  EXPECT_TRUE(HasArg(arg_set_id, storage_->InternString("data"),
                      Variadic::String(storage_->InternString(kFullData))));
 }
 
@@ -2645,15 +2501,15 @@
   context_.sorter->ExtractEventsForced();
 
   // Verify raw_table and args contents.
-  const auto& raw_table = storage_->raw_table();
+  const auto& raw_table = storage_->chrome_raw_table();
   EXPECT_EQ(raw_table.row_count(), 1u);
   EXPECT_EQ(raw_table[0].name(),
             storage_->InternString("chrome_event.legacy_user_trace"));
-  EXPECT_EQ(raw_table[0].arg_set_id(), 1u);
 
+  uint32_t arg_set_id = raw_table[0].arg_set_id();
   EXPECT_EQ(storage_->arg_table().row_count(), 1u);
   EXPECT_TRUE(
-      HasArg(1u, storage_->InternString("data"),
+      HasArg(arg_set_id, storage_->InternString("data"),
              Variadic::String(storage_->InternString(kUserTraceEvent))));
 }
 
diff --git a/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc b/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
index 166640c..7c111ce 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader_unittest.cc
@@ -44,7 +44,7 @@
     proto_trace_reader_ = std::make_unique<ProtoTraceReader>(&context_);
   }
 
-  util::Status Tokenize() {
+  base::Status Tokenize() {
     trace_->Finalize();
     std::vector<uint8_t> trace_bytes = trace_.SerializeAsArray();
     std::unique_ptr<uint8_t[]> raw_trace(new uint8_t[trace_bytes.size()]);
diff --git a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
index b6b5937..67414bc 100644
--- a/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
+++ b/src/trace_processor/importers/proto/proto_trace_tokenizer.cc
@@ -24,7 +24,7 @@
 
 ProtoTraceTokenizer::ProtoTraceTokenizer() = default;
 
-util::Status ProtoTraceTokenizer::Decompress(TraceBlobView input,
+base::Status ProtoTraceTokenizer::Decompress(TraceBlobView input,
                                              TraceBlobView* output) {
   PERFETTO_DCHECK(util::IsGzipSupported());
 
@@ -41,13 +41,13 @@
       });
 
   if (ret == ResultCode::kError || ret == ResultCode::kNeedsMoreInput) {
-    return util::ErrStatus("Failed to decompress (error code: %d)",
+    return base::ErrStatus("Failed to decompress (error code: %d)",
                            static_cast<int>(ret));
   }
 
   TraceBlob out_blob = TraceBlob::CopyFrom(data.data(), data.size());
   *output = TraceBlobView(std::move(out_blob));
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/statsd_module.cc b/src/trace_processor/importers/proto/statsd_module.cc
index 36d4fb3..5897f7b 100644
--- a/src/trace_processor/importers/proto/statsd_module.cc
+++ b/src/trace_processor/importers/proto/statsd_module.cc
@@ -37,7 +37,6 @@
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/common/tracks.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
@@ -108,23 +107,17 @@
 using perfetto::protos::pbzero::StatsdAtom;
 using perfetto::protos::pbzero::TracePacket;
 
-PoolAndDescriptor::PoolAndDescriptor(const uint8_t* data,
-                                     size_t size,
-                                     const char* name) {
-  pool_.AddFromFileDescriptorSet(data, size);
-  std::optional<uint32_t> opt_idx = pool_.FindDescriptorIdx(name);
-  if (opt_idx.has_value()) {
-    descriptor_ = &pool_.descriptors()[opt_idx.value()];
-  }
-}
-
-PoolAndDescriptor::~PoolAndDescriptor() = default;
-
 StatsdModule::StatsdModule(TraceProcessorContext* context)
-    : context_(context),
-      pool_(kAtomsDescriptor.data(), kAtomsDescriptor.size(), kAtomProtoName),
-      args_parser_(*(pool_.pool())) {
+    : context_(context), args_parser_(*context_->descriptor_pool_) {
   RegisterForField(TracePacket::kStatsdAtomFieldNumber, context);
+  context_->descriptor_pool_->AddFromFileDescriptorSet(kAtomsDescriptor.data(),
+                                                       kAtomsDescriptor.size());
+
+  std::optional<uint32_t> opt_idx =
+      context_->descriptor_pool_->FindDescriptorIdx(kAtomProtoName);
+  if (opt_idx.has_value()) {
+    descriptor_ = &context_->descriptor_pool_->descriptors()[opt_idx.value()];
+  }
 }
 
 StatsdModule::~StatsdModule() = default;
@@ -204,7 +197,7 @@
       [&](ArgsTracker::BoundInserter* inserter) {
         ArgsParser delegate(ts, *inserter, *context_->storage);
 
-        const auto& fields = pool_.descriptor()->fields();
+        const auto& fields = descriptor_->fields();
         const auto& field_it = fields.find(nested_field_id);
         base::Status status;
 
@@ -232,13 +225,13 @@
 StringId StatsdModule::GetAtomName(uint32_t atom_field_id) {
   StringId* cached_name = atom_names_.Find(atom_field_id);
   if (cached_name == nullptr) {
-    if (pool_.descriptor() == nullptr) {
+    if (descriptor_ == nullptr) {
       context_->storage->IncrementStats(stats::atom_unknown);
       return context_->storage->InternString("Could not load atom descriptor");
     }
 
     StringId name_id;
-    const auto& fields = pool_.descriptor()->fields();
+    const auto& fields = descriptor_->fields();
     const auto& field_it = fields.find(atom_field_id);
     if (field_it == fields.end()) {
       base::StackString<255> name("atom_%u", atom_field_id);
diff --git a/src/trace_processor/importers/proto/statsd_module.h b/src/trace_processor/importers/proto/statsd_module.h
index bfcc86c..1d8658a 100644
--- a/src/trace_processor/importers/proto/statsd_module.h
+++ b/src/trace_processor/importers/proto/statsd_module.h
@@ -25,7 +25,6 @@
 #include "perfetto/trace_processor/ref_counted.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -35,27 +34,6 @@
 
 namespace perfetto::trace_processor {
 
-// Wraps a DescriptorPool and a pointer into that pool. This prevents
-// common bugs where moving/changing the pool invalidates the pointer.
-class PoolAndDescriptor {
- public:
-  PoolAndDescriptor(const uint8_t* data, size_t size, const char* name);
-  virtual ~PoolAndDescriptor();
-
-  PoolAndDescriptor(const PoolAndDescriptor&) = delete;
-  PoolAndDescriptor& operator=(const PoolAndDescriptor&) = delete;
-  PoolAndDescriptor(PoolAndDescriptor&&) = delete;
-  PoolAndDescriptor& operator=(PoolAndDescriptor&&) = delete;
-
-  const DescriptorPool* pool() const { return &pool_; }
-
-  const ProtoDescriptor* descriptor() const { return descriptor_; }
-
- private:
-  DescriptorPool pool_;
-  const ProtoDescriptor* descriptor_{};
-};
-
 class StatsdModule : public ProtoImporterModule {
  public:
   explicit StatsdModule(TraceProcessorContext* context);
@@ -80,7 +58,7 @@
 
   TraceProcessorContext* context_;
   base::FlatHashMap<uint32_t, StringId> atom_names_;
-  PoolAndDescriptor pool_;
+  const ProtoDescriptor* descriptor_ = nullptr;
   util::ProtoToArgsParser args_parser_;
   std::optional<TrackId> track_id_;
 };
diff --git a/src/trace_processor/importers/proto/track_event_module.cc b/src/trace_processor/importers/proto/track_event_module.cc
index 0ae00f2..80912f5 100644
--- a/src/trace_processor/importers/proto/track_event_module.cc
+++ b/src/trace_processor/importers/proto/track_event_module.cc
@@ -22,8 +22,11 @@
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/proto/android_track_event.descriptor.h"
+#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
+#include "src/trace_processor/importers/proto/track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
@@ -42,6 +45,13 @@
   RegisterForField(TracePacket::kTrackDescriptorFieldNumber, context);
   RegisterForField(TracePacket::kThreadDescriptorFieldNumber, context);
   RegisterForField(TracePacket::kProcessDescriptorFieldNumber, context);
+
+  context->descriptor_pool_->AddFromFileDescriptorSet(
+      kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
+  context->descriptor_pool_->AddFromFileDescriptorSet(
+      kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
+  context->descriptor_pool_->AddFromFileDescriptorSet(
+      kAndroidTrackEventDescriptor.data(), kAndroidTrackEventDescriptor.size());
 }
 
 TrackEventModule::~TrackEventModule() = default;
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 4833a74..b9ea5e5 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -53,6 +53,7 @@
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
 #include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/debug_annotation_parser.h"
@@ -66,7 +67,6 @@
 #include "protos/perfetto/trace/track_event/chrome_histogram_sample.pbzero.h"
 #include "protos/perfetto/trace/track_event/chrome_process_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/chrome_thread_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "protos/perfetto/trace/track_event/log_message.pbzero.h"
 #include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"
@@ -450,7 +450,8 @@
         upid_ = storage_->thread_table()[*utid_].upid();
         track_id_ = track_tracker->InternThreadTrack(*utid_);
       } else {
-        track_id_ = track_event_tracker_->GetOrCreateDefaultDescriptorTrack();
+        track_id_ = *track_event_tracker_->GetDescriptorTrack(
+            TrackEventTracker::kDefaultDescriptorTrackUuid);
       }
     }
 
@@ -689,12 +690,18 @@
           "TrackEvent with phase B without thread association");
     }
 
-    auto* thread_slices = storage_->mutable_slice_table();
-    auto opt_slice_id = context_->slice_tracker->BeginTyped(
-        thread_slices, MakeThreadSliceRow(),
+    auto opt_slice_id = context_->slice_tracker->Begin(
+        ts_, track_id_, category_id_, name_id_,
         [this](BoundInserter* inserter) { ParseTrackEventArgs(inserter); });
-
     if (opt_slice_id.has_value()) {
+      auto rr =
+          context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
+      if (thread_timestamp_) {
+        rr->set_thread_ts(*thread_timestamp_);
+      }
+      if (thread_instruction_count_) {
+        rr->set_thread_instruction_count(*thread_instruction_count_);
+      }
       MaybeParseFlowEvents(opt_slice_id.value());
     }
     return base::OkStatus();
@@ -747,20 +754,22 @@
     if (duration_ns < 0)
       return base::ErrStatus("TrackEvent with phase X with negative duration");
 
-    auto* thread_slices = storage_->mutable_slice_table();
-    tables::SliceTable::Row row = MakeThreadSliceRow();
-    row.dur = duration_ns;
-    if (legacy_event_.has_thread_duration_us()) {
-      row.thread_dur = legacy_event_.thread_duration_us() * 1000;
-    }
-    if (legacy_event_.has_thread_instruction_delta()) {
-      row.thread_instruction_delta = legacy_event_.thread_instruction_delta();
-    }
-    auto opt_slice_id = context_->slice_tracker->ScopedTyped(
-        thread_slices, row,
+    auto opt_slice_id = context_->slice_tracker->Scoped(
+        ts_, track_id_, category_id_, name_id_, duration_ns,
         [this](BoundInserter* inserter) { ParseTrackEventArgs(inserter); });
-
     if (opt_slice_id.has_value()) {
+      auto rr =
+          context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
+      PERFETTO_CHECK(rr);
+      if (thread_timestamp_) {
+        rr->set_thread_ts(*thread_timestamp_);
+        rr->set_thread_dur(legacy_event_.thread_duration_us() * 1000);
+      }
+      if (thread_instruction_count_) {
+        rr->set_thread_instruction_count(*thread_instruction_count_);
+        rr->set_thread_instruction_delta(
+            legacy_event_.thread_instruction_delta());
+      }
       MaybeParseFlowEvents(opt_slice_id.value());
     }
     return base::OkStatus();
@@ -879,25 +888,23 @@
                          Variadic::String(phase_id));
       }
     };
+    opt_slice_id =
+        context_->slice_tracker->Scoped(ts_, track_id_, category_id_, name_id_,
+                                        duration_ns, std::move(args_inserter));
+    if (!opt_slice_id) {
+      return base::OkStatus();
+    }
     if (utid_) {
-      auto* thread_slices = storage_->mutable_slice_table();
-      tables::SliceTable::Row row = MakeThreadSliceRow();
-      row.dur = duration_ns;
+      auto rr =
+          context_->storage->mutable_slice_table()->FindById(*opt_slice_id);
       if (thread_timestamp_) {
-        row.thread_dur = duration_ns;
+        rr->set_thread_ts(*thread_timestamp_);
+        rr->set_thread_dur(duration_ns);
       }
       if (thread_instruction_count_) {
-        row.thread_instruction_delta = tidelta;
+        rr->set_thread_instruction_count(*thread_instruction_count_);
+        rr->set_thread_instruction_delta(tidelta);
       }
-      opt_slice_id = context_->slice_tracker->ScopedTyped(
-          thread_slices, row, std::move(args_inserter));
-    } else {
-      opt_slice_id = context_->slice_tracker->Scoped(
-          ts_, track_id_, category_id_, name_id_, duration_ns,
-          std::move(args_inserter));
-    }
-    if (!opt_slice_id.has_value()) {
-      return base::OkStatus();
     }
     MaybeParseFlowEvents(opt_slice_id.value());
     return base::OkStatus();
@@ -1059,10 +1066,9 @@
     if (!utid_)
       return base::ErrStatus("raw legacy event without thread association");
 
-    auto ucpu = context_->cpu_tracker->GetOrCreateCpu(0);
-    RawId id =
-        storage_->mutable_raw_table()
-            ->Insert({ts_, parser_->raw_legacy_event_id_, *utid_, 0, 0, ucpu})
+    tables::ChromeRawTable::Id id =
+        storage_->mutable_chrome_raw_table()
+            ->Insert({ts_, parser_->raw_legacy_event_id_, *utid_, 0})
             .id;
 
     auto inserter = context_->args_tracker->AddArgsTo(id);
@@ -1336,19 +1342,6 @@
     return base::OkStatus();
   }
 
-  tables::SliceTable::Row MakeThreadSliceRow() {
-    tables::SliceTable::Row row;
-    row.ts = ts_;
-    row.track_id = track_id_;
-    row.category = category_id_;
-    row.name = name_id_;
-    row.thread_ts = thread_timestamp_;
-    row.thread_dur = std::nullopt;
-    row.thread_instruction_count = thread_instruction_count_;
-    row.thread_instruction_delta = std::nullopt;
-    return row;
-  }
-
   TraceProcessorContext* context_;
   TrackEventTracker* track_event_tracker_;
   TraceStorage* storage_;
@@ -1554,13 +1547,15 @@
 
   if (decoder.has_thread()) {
     UniqueTid utid = ParseThreadDescriptor(decoder.thread());
-    if (decoder.has_chrome_thread())
+    if (decoder.has_chrome_thread()) {
       ParseChromeThreadDescriptor(utid, decoder.chrome_thread());
+    }
   } else if (decoder.has_process()) {
     UniquePid upid =
         ParseProcessDescriptor(packet_timestamp, decoder.process());
-    if (decoder.has_chrome_process())
+    if (decoder.has_chrome_process()) {
       ParseChromeProcessDescriptor(upid, decoder.chrome_process());
+    }
   }
 
   // Override the name with the most recent name seen (after sorting by ts).
@@ -1572,7 +1567,7 @@
   } else if (decoder.has_atrace_name()) {
     name = decoder.atrace_name();
   }
-  if (name.data != nullptr) {
+  if (name.data) {
     auto* tracks = context_->storage->mutable_track_table();
     const StringId raw_name_id = context_->storage->InternString(name);
     const StringId name_id =
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index bc7417f..7f77d60 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -88,7 +88,7 @@
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
     return ModuleResult::Handled();
   }
-  track_event_tracker_->SetRangeOfInterestStartUs(range_of_interest.start_us());
+  track_event_tracker_->set_range_of_interest_us(range_of_interest.start_us());
   context_->metadata_tracker->SetMetadata(
       metadata::range_of_interest_start_us,
       Variadic::Integer(range_of_interest.start_us()));
@@ -241,9 +241,12 @@
     auto unit = static_cast<uint32_t>(counter.unit());
     if (counter.type() == CounterDescriptor::COUNTER_THREAD_TIME_NS) {
       counter_details.unit = counter_unit_ids_[CounterDescriptor::UNIT_TIME_NS];
+      counter_details.builtin_type_str = counter_name_thread_time_id_;
     } else if (counter.type() ==
                CounterDescriptor::COUNTER_THREAD_INSTRUCTION_COUNT) {
       counter_details.unit = counter_unit_ids_[CounterDescriptor::UNIT_COUNT];
+      counter_details.builtin_type_str =
+          counter_name_thread_instruction_count_id_;
     } else if (unit < counter_unit_ids_.size() &&
                unit != CounterDescriptor::COUNTER_UNSPECIFIED) {
       counter_details.unit = counter_unit_ids_[unit];
diff --git a/src/trace_processor/importers/proto/track_event_tracker.cc b/src/trace_processor/importers/proto/track_event_tracker.cc
index 227b3d8..3a5c213 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.cc
+++ b/src/trace_processor/importers/proto/track_event_tracker.cc
@@ -17,15 +17,13 @@
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 
 #include <algorithm>
+#include <array>
 #include <cinttypes>
 #include <cstddef>
 #include <cstdint>
-#include <map>
 #include <memory>
 #include <optional>
-#include <tuple>
-#include <utility>
-#include <vector>
+#include <unordered_set>
 
 #include "perfetto/base/logging.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -59,7 +57,7 @@
     tracks::DynamicNameBlueprint());
 
 constexpr auto kGlobalCounterTrackBlueprint = tracks::CounterBlueprint(
-    "global_track_event",
+    "global_counter_track_event",
     tracks::DynamicUnitBlueprint(),
     tracks::DimensionBlueprints(tracks::LongDimensionBlueprint("track_uuid")),
     tracks::DynamicNameBlueprint());
@@ -88,6 +86,8 @@
       source_id_key_(context->storage->InternString("trace_id")),
       is_root_in_scope_key_(context->storage->InternString("is_root_in_scope")),
       category_key_(context->storage->InternString("category")),
+      builtin_counter_type_key_(
+          context->storage->InternString("builtin_counter_type")),
       has_first_packet_on_sequence_key_id_(
           context->storage->InternString("has_first_packet_on_sequence")),
       child_ordering_key_(context->storage->InternString("child_ordering")),
@@ -104,293 +104,165 @@
 void TrackEventTracker::ReserveDescriptorTrack(
     uint64_t uuid,
     const DescriptorTrackReservation& reservation) {
-  std::map<uint64_t, DescriptorTrackReservation>::iterator it;
-  bool inserted;
-  std::tie(it, inserted) =
-      reserved_descriptor_tracks_.insert(std::make_pair<>(uuid, reservation));
-
-  if (inserted)
+  if (uuid == kDefaultDescriptorTrackUuid && reservation.parent_uuid) {
+    PERFETTO_DLOG(
+        "Default track (uuid 0) cannot have a parent uui specified. Ignoring "
+        "the descriptor.");
+    context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
     return;
+  }
 
-  if (!it->second.IsForSameTrack(reservation)) {
-    PERFETTO_DLOG("New track reservation for process track with uuid %" PRIu64
+  auto [it, inserted] = reserved_descriptor_tracks_.Insert(uuid, reservation);
+  if (inserted) {
+    return;
+  }
+
+  if (!it->IsForSameTrack(reservation)) {
+    PERFETTO_DLOG("New track reservation for track with uuid %" PRIu64
                   " doesn't match earlier one",
                   uuid);
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
     return;
   }
-  it->second.min_timestamp =
-      std::min(it->second.min_timestamp, reservation.min_timestamp);
+  it->min_timestamp = std::min(it->min_timestamp, reservation.min_timestamp);
 }
 
-std::optional<TrackId> TrackEventTracker::GetDescriptorTrack(
+std::optional<TrackEventTracker::ResolvedDescriptorTrack>
+TrackEventTracker::GetDescriptorTrackImpl(
     uint64_t uuid,
     StringId event_name,
     std::optional<uint32_t> packet_sequence_id) {
-  std::optional<TrackId> track_id =
-      GetDescriptorTrackImpl(uuid, packet_sequence_id);
-  if (!track_id || event_name.is_null())
-    return track_id;
+  auto* resolved_ptr = resolved_descriptor_tracks_.Find(uuid);
+  if (resolved_ptr) {
+    if (event_name.is_null()) {
+      return *resolved_ptr;
+    }
 
-  // Update the name of the track if unset and the track is not the primary
-  // track of a process/thread or a counter track.
-  auto rr = *context_->storage->mutable_track_table()->FindById(*track_id);
-  if (!rr.name().is_null()) {
-    return track_id;
-  }
-
-  // Check reservation for track type.
-  auto reservation_it = reserved_descriptor_tracks_.find(uuid);
-  PERFETTO_CHECK(reservation_it != reserved_descriptor_tracks_.end());
-
-  if (reservation_it->second.pid || reservation_it->second.tid ||
-      reservation_it->second.is_counter) {
-    return track_id;
-  }
-  rr.set_name(
-      context_->process_track_translation_table->TranslateName(event_name));
-  return track_id;
-}
-
-std::optional<TrackId> TrackEventTracker::GetDescriptorTrackImpl(
-    uint64_t uuid,
-    std::optional<uint32_t> packet_sequence_id) {
-  auto it = descriptor_tracks_.find(uuid);
-  if (it != descriptor_tracks_.end())
-    return it->second;
-
-  std::optional<ResolvedDescriptorTrack> resolved_track =
-      ResolveDescriptorTrack(uuid, nullptr);
-  if (!resolved_track)
-    return std::nullopt;
-
-  // The reservation must exist as |resolved_track| would have been std::nullopt
-  // otherwise.
-  auto reserved_it = reserved_descriptor_tracks_.find(uuid);
-  PERFETTO_CHECK(reserved_it != reserved_descriptor_tracks_.end());
-
-  const auto& reservation = reserved_it->second;
-
-  // We resolve parent_id here to ensure that it's going to be smaller
-  // than the id of the child.
-  std::optional<TrackId> parent_id;
-  if (reservation.parent_uuid != 0) {
-    parent_id = GetDescriptorTrackImpl(reservation.parent_uuid);
-  }
-
-  TrackId track_id = CreateTrackFromResolved(uuid, packet_sequence_id,
-                                             reservation, *resolved_track);
-  descriptor_tracks_[uuid] = track_id;
-
-  auto row_ref = *context_->storage->mutable_track_table()->FindById(track_id);
-  if (!row_ref.source_arg_set_id().has_value()) {
-    auto inserter = context_->args_tracker->AddArgsTo(track_id);
-    AddTrackArgs(uuid, packet_sequence_id, reservation, *resolved_track,
-                 inserter);
-  }
-  if (parent_id) {
-    row_ref.set_parent_id(*parent_id);
-  }
-  if (!reservation.name.is_null()) {
-    // Initialize the track name here, so that, if a name was given in the
-    // reservation, it is set immediately after resolution takes place.
-    row_ref.set_name(reservation.name);
-  }
-  return track_id;
-}
-
-TrackId TrackEventTracker::CreateTrackFromResolved(
-    uint64_t uuid,
-    std::optional<uint32_t> packet_sequence_id,
-    const DescriptorTrackReservation& reservation,
-    const ResolvedDescriptorTrack& track) {
-  if (track.is_root_in_scope()) {
-    switch (track.scope()) {
-      case ResolvedDescriptorTrack::Scope::kThread: {
-        if (track.use_separate_track()) {
-          auto it = thread_tracks_.find(track.utid());
-          if (it != thread_tracks_.end()) {
-            return it->second;
-          }
-          TrackId id = context_->track_tracker->InternTrack(
-              kThreadTrackBlueprint,
-              tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
-              tracks::DynamicName(kNullStringId));
-          thread_tracks_[track.utid()] = id;
-          return id;
-        }
-        return context_->track_tracker->InternThreadTrack(track.utid());
+    // Update the name to match the first non-null and valid event name. We need
+    // this because TrackEventParser calls |GetDescriptorTrack| with
+    // kNullStringId which means we cannot just have the code below for updating
+    // the name
+    DescriptorTrackReservation* reservation_ptr =
+        reserved_descriptor_tracks_.Find(uuid);
+    PERFETTO_CHECK(reservation_ptr);
+    auto* tracks = context_->storage->mutable_track_table();
+    auto rr = *tracks->FindById(resolved_ptr->track_id());
+    bool is_root_thread_process_or_counter = reservation_ptr->pid ||
+                                             reservation_ptr->tid ||
+                                             reservation_ptr->is_counter;
+    if (rr.name().is_null() && !is_root_thread_process_or_counter) {
+      if (resolved_ptr->scope() == ResolvedDescriptorTrack::Scope::kProcess) {
+        rr.set_name(context_->process_track_translation_table->TranslateName(
+            event_name));
+      } else {
+        rr.set_name(event_name);
       }
-      case ResolvedDescriptorTrack::Scope::kProcess: {
-        return context_->track_tracker->InternTrack(
-            kProcessTrackBlueprint,
-            tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
-            tracks::DynamicName(kNullStringId));
-      }
-      case ResolvedDescriptorTrack::Scope::kGlobal:
-        // Will be handled below.
-        break;
     }
+    return *resolved_ptr;
   }
 
-  if (track.is_counter()) {
-    switch (track.scope()) {
-      case ResolvedDescriptorTrack::Scope::kThread:
-        return context_->track_tracker->InternTrack(
-            kThreadCounterTrackBlueprint,
-            tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
-            tracks::DynamicName(kNullStringId),
-            [&, this](ArgsTracker::BoundInserter& inserter) {
-              AddTrackArgs(uuid, packet_sequence_id, reservation, track,
-                           inserter);
-            },
-            tracks::DynamicUnit(reservation.counter_details->unit));
-      case ResolvedDescriptorTrack::Scope::kProcess:
-        return context_->track_tracker->InternTrack(
-            kProcessCounterTrackBlueprint,
-            tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
-            tracks::DynamicName(kNullStringId),
-            [&, this](ArgsTracker::BoundInserter& inserter) {
-              AddTrackArgs(uuid, packet_sequence_id, reservation, track,
-                           inserter);
-            },
-            tracks::DynamicUnit(reservation.counter_details->unit));
-      case ResolvedDescriptorTrack::Scope::kGlobal:
-        return context_->track_tracker->InternTrack(
-            kGlobalCounterTrackBlueprint,
-            tracks::Dimensions(static_cast<int64_t>(uuid)),
-            tracks::DynamicName(kNullStringId),
-            [&, this](ArgsTracker::BoundInserter& inserter) {
-              AddTrackArgs(uuid, packet_sequence_id, reservation, track,
-                           inserter);
-            },
-            tracks::DynamicUnit(reservation.counter_details->unit));
+  DescriptorTrackReservation* reservation_ptr =
+      reserved_descriptor_tracks_.Find(uuid);
+  if (!reservation_ptr) {
+    if (uuid != kDefaultDescriptorTrackUuid) {
+      return std::nullopt;
     }
+
+    // For the default track, if there's no reservation, forcefully create it
+    // as it's always allowed to emit events on it, even without emitting a
+    // descriptor.
+    DescriptorTrackReservation r;
+    r.parent_uuid = 0;
+    r.name = default_descriptor_track_name_;
+    ReserveDescriptorTrack(kDefaultDescriptorTrackUuid, r);
+
+    reservation_ptr = reserved_descriptor_tracks_.Find(uuid);
+    PERFETTO_CHECK(reservation_ptr);
   }
 
-  switch (track.scope()) {
-    case ResolvedDescriptorTrack::Scope::kThread: {
-      return context_->track_tracker->InternTrack(
-          kThreadTrackBlueprint,
-          tracks::Dimensions(track.utid(), static_cast<int64_t>(uuid)),
-          tracks::DynamicName(kNullStringId));
-    }
-    case ResolvedDescriptorTrack::Scope::kProcess: {
-      return context_->track_tracker->InternTrack(
-          kProcessTrackBlueprint,
-          tracks::Dimensions(track.upid(), static_cast<int64_t>(uuid)),
-          tracks::DynamicName(kNullStringId));
-    }
-    case ResolvedDescriptorTrack::Scope::kGlobal: {
-      return context_->track_tracker->InternTrack(
-          kGlobalTrackBlueprint, tracks::Dimensions(static_cast<int64_t>(uuid)),
-          tracks::DynamicName(kNullStringId));
-    }
-  }
-  PERFETTO_FATAL("For GCC");
-}
-
-std::optional<TrackEventTracker::ResolvedDescriptorTrack>
-TrackEventTracker::ResolveDescriptorTrack(
-    uint64_t uuid,
-    std::vector<uint64_t>* descendent_uuids) {
-  auto it = resolved_descriptor_tracks_.find(uuid);
-  if (it != resolved_descriptor_tracks_.end())
-    return it->second;
-
-  auto reservation_it = reserved_descriptor_tracks_.find(uuid);
-  if (reservation_it == reserved_descriptor_tracks_.end())
+  // Before trying to resolve anything, ensure that the hierarchy of tracks is
+  // well defined.
+  if (!IsTrackHierarchyValid(uuid)) {
     return std::nullopt;
+  }
 
   // Resolve process and thread id for tracks produced from within a pid
   // namespace.
+  //
   // Get the root-level trusted_pid for the process that produces the track
   // event.
-  auto opt_trusted_pid = context_->process_tracker->GetTrustedPid(uuid);
-  auto& reservation = reservation_it->second;
+  std::optional<uint32_t> trusted_pid =
+      context_->process_tracker->GetTrustedPid(uuid);
+  DescriptorTrackReservation& reservation = *reservation_ptr;
+
   // Try to resolve to root-level pid and tid if the process is pid-namespaced.
-  if (opt_trusted_pid && reservation.tid) {
-    auto opt_resolved_tid = context_->process_tracker->ResolveNamespacedTid(
-        *opt_trusted_pid, *reservation.tid);
-    if (opt_resolved_tid)
-      reservation.tid = *opt_resolved_tid;
+  if (trusted_pid && reservation.tid) {
+    std::optional<uint32_t> resolved_tid =
+        context_->process_tracker->ResolveNamespacedTid(*trusted_pid,
+                                                        *reservation.tid);
+    if (resolved_tid) {
+      reservation.tid = *resolved_tid;
+    }
   }
-  if (opt_trusted_pid && reservation.pid) {
-    auto opt_resolved_pid = context_->process_tracker->ResolveNamespacedTid(
-        *opt_trusted_pid, *reservation.pid);
-    if (opt_resolved_pid)
-      reservation.pid = *opt_resolved_pid;
+  if (trusted_pid && reservation.pid) {
+    std::optional<uint32_t> resolved_pid =
+        context_->process_tracker->ResolveNamespacedTid(*trusted_pid,
+                                                        *reservation.pid);
+    if (resolved_pid) {
+      reservation.pid = *resolved_pid;
+    }
   }
 
-  std::optional<ResolvedDescriptorTrack> resolved_track =
-      ResolveDescriptorTrackImpl(uuid, reservation, descendent_uuids);
-  if (!resolved_track) {
-    return std::nullopt;
+  bool is_root_thread_process_or_counter = reservation_ptr->pid ||
+                                           reservation_ptr->tid ||
+                                           reservation_ptr->is_counter;
+  if (reservation.name.is_null() && !is_root_thread_process_or_counter) {
+    reservation.name = event_name;
   }
-  resolved_descriptor_tracks_[uuid] = *resolved_track;
-  return resolved_track;
+
+  // If the reservation does not have a name specified, name it the same
+  // as the first event on the track. Note this only applies for non-root and
+  // non-counter tracks.
+  auto [it, inserted] = resolved_descriptor_tracks_.Insert(
+      uuid, ResolveDescriptorTrack(uuid, reservation, packet_sequence_id));
+  PERFETTO_CHECK(inserted);
+  return *it;
 }
 
-std::optional<TrackEventTracker::ResolvedDescriptorTrack>
-TrackEventTracker::ResolveDescriptorTrackImpl(
+TrackEventTracker::ResolvedDescriptorTrack
+TrackEventTracker::ResolveDescriptorTrack(
     uint64_t uuid,
     const DescriptorTrackReservation& reservation,
-    std::vector<uint64_t>* descendent_uuids) {
-  static constexpr size_t kMaxAncestors = 10;
+    std::optional<uint32_t> packet_sequence_id) {
+  TrackTracker::SetArgsCallback args_fn_root =
+      [&, this](ArgsTracker::BoundInserter& inserter) {
+        AddTrackArgs(uuid, packet_sequence_id, reservation, true /* is_root*/,
+                     inserter);
+      };
+  TrackTracker::SetArgsCallback args_fn_non_root =
+      [&, this](ArgsTracker::BoundInserter& inserter) {
+        AddTrackArgs(uuid, packet_sequence_id, reservation, false /* is_root*/,
+                     inserter);
+      };
 
   // Try to resolve any parent tracks recursively, too.
   std::optional<ResolvedDescriptorTrack> parent_resolved_track;
-  if (reservation.parent_uuid) {
-    // Input data may contain loops or extremely long ancestor track chains. To
-    // avoid stack overflow in these situations, we keep track of the ancestors
-    // seen in the recursion.
-    std::unique_ptr<std::vector<uint64_t>> owned_descendent_uuids;
-    if (!descendent_uuids) {
-      owned_descendent_uuids = std::make_unique<std::vector<uint64_t>>();
-      descendent_uuids = owned_descendent_uuids.get();
-    }
-    descendent_uuids->push_back(uuid);
-
-    if (descendent_uuids->size() > kMaxAncestors) {
-      PERFETTO_ELOG(
-          "Too many ancestors in parent_track_uuid hierarchy at track %" PRIu64
-          " with parent %" PRIu64,
-          uuid, reservation.parent_uuid);
-      return std::nullopt;
-    }
-
-    if (std::find(descendent_uuids->begin(), descendent_uuids->end(),
-                  reservation.parent_uuid) != descendent_uuids->end()) {
-      PERFETTO_ELOG(
-          "Loop detected in parent_track_uuid hierarchy at track %" PRIu64
-          " with parent %" PRIu64,
-          uuid, reservation.parent_uuid);
-      return std::nullopt;
-    }
-
-    parent_resolved_track =
-        ResolveDescriptorTrack(reservation.parent_uuid, descendent_uuids);
-    if (!parent_resolved_track) {
-      PERFETTO_ELOG("Unknown parent track %" PRIu64 " for track %" PRIu64,
-                    reservation.parent_uuid, uuid);
-    }
-
-    descendent_uuids->pop_back();
-    if (owned_descendent_uuids)
-      descendent_uuids = nullptr;
+  if (reservation.parent_uuid != kDefaultDescriptorTrackUuid) {
+    parent_resolved_track = GetDescriptorTrackImpl(
+        reservation.parent_uuid, kNullStringId, packet_sequence_id);
   }
 
   if (reservation.tid) {
     UniqueTid utid = context_->process_tracker->UpdateThread(*reservation.tid,
                                                              *reservation.pid);
-    auto it_and_inserted =
-        descriptor_uuids_by_utid_.insert(std::make_pair<>(utid, uuid));
-    if (!it_and_inserted.second) {
+    auto [it, inserted] = descriptor_uuids_by_utid_.Insert(utid, uuid);
+    if (!inserted) {
       // We already saw a another track with a different uuid for this thread.
       // Since there should only be one descriptor track for each thread, we
       // assume that its tid was reused. So, start a new thread.
-      uint64_t old_uuid = it_and_inserted.first->second;
+      uint64_t old_uuid = *it;
       PERFETTO_DCHECK(old_uuid != uuid);  // Every track is only resolved once.
+      *it = uuid;
 
       PERFETTO_DLOG("Detected tid reuse (pid: %" PRIu32 " tid: %" PRIu32
                     ") from track descriptors (old uuid: %" PRIu64
@@ -398,31 +270,46 @@
                     *reservation.pid, *reservation.tid, old_uuid, uuid,
                     reservation.min_timestamp);
 
+      // Associate the new thread with its process.
       utid = context_->process_tracker->StartNewThread(std::nullopt,
                                                        *reservation.tid);
-
-      // Associate the new thread with its process.
-      PERFETTO_CHECK(context_->process_tracker->UpdateThread(
-                         *reservation.tid, *reservation.pid) == utid);
-
-      descriptor_uuids_by_utid_[utid] = uuid;
+      UniqueTid updated_utid = context_->process_tracker->UpdateThread(
+          *reservation.tid, *reservation.pid);
+      PERFETTO_CHECK(updated_utid == utid);
     }
-    return ResolvedDescriptorTrack::Thread(utid, false /* is_counter */,
-                                           true /* is_root*/,
-                                           reservation.use_separate_track);
+
+    TrackId id;
+    if (reservation.is_counter) {
+      id = context_->track_tracker->InternTrack(
+          kThreadCounterTrackBlueprint,
+          tracks::Dimensions(utid, static_cast<int64_t>(uuid)),
+          tracks::DynamicName(reservation.name), args_fn_root,
+          tracks::DynamicUnit(reservation.counter_details->unit));
+    } else if (reservation.use_separate_track) {
+      id = context_->track_tracker->InternTrack(
+          kThreadTrackBlueprint,
+          tracks::Dimensions(utid, static_cast<int64_t>(uuid)),
+          tracks::DynamicName(reservation.name), args_fn_root);
+    } else {
+      id = context_->track_tracker->InternThreadTrack(utid);
+      return ResolvedDescriptorTrack::Thread(id, utid, reservation.is_counter,
+                                             true);
+    }
+    return ResolvedDescriptorTrack::Thread(id, utid, reservation.is_counter,
+                                           false);
   }
 
   if (reservation.pid) {
     UniquePid upid =
         context_->process_tracker->GetOrCreateProcess(*reservation.pid);
-    auto it_and_inserted =
-        descriptor_uuids_by_upid_.insert(std::make_pair<>(upid, uuid));
-    if (!it_and_inserted.second) {
+    auto [it, inserted] = descriptor_uuids_by_upid_.Insert(upid, uuid);
+    if (!inserted) {
       // We already saw a another track with a different uuid for this process.
-      // Since there should only be one descriptor track for each process, we
-      // assume that its pid was reused. So, start a new process.
-      uint64_t old_uuid = it_and_inserted.first->second;
+      // Since there should only be one descriptor track for each process,
+      // we assume that its pid was reused. So, start a new process.
+      uint64_t old_uuid = *it;
       PERFETTO_DCHECK(old_uuid != uuid);  // Every track is only resolved once.
+      *it = uuid;
 
       PERFETTO_DLOG("Detected pid reuse (pid: %" PRIu32
                     ") from track descriptors (old uuid: %" PRIu64
@@ -433,85 +320,127 @@
       upid = context_->process_tracker->StartNewProcess(
           std::nullopt, std::nullopt, *reservation.pid, kNullStringId,
           ThreadNamePriority::kTrackDescriptor);
-
-      descriptor_uuids_by_upid_[upid] = uuid;
     }
-    return ResolvedDescriptorTrack::Process(upid, false /* is_counter */,
-                                            true /* is_root*/);
+    StringId translated_name =
+        context_->process_track_translation_table->TranslateName(
+            reservation.name);
+    TrackId id;
+    if (reservation.is_counter) {
+      id = context_->track_tracker->InternTrack(
+          kProcessCounterTrackBlueprint,
+          tracks::Dimensions(upid, static_cast<int64_t>(uuid)),
+          tracks::DynamicName(translated_name), args_fn_root,
+          tracks::DynamicUnit(reservation.counter_details->unit));
+    } else {
+      id = context_->track_tracker->InternTrack(
+          kProcessTrackBlueprint,
+          tracks::Dimensions(upid, static_cast<int64_t>(uuid)),
+          tracks::DynamicName(translated_name), args_fn_root);
+    }
+    return ResolvedDescriptorTrack::Process(id, upid, reservation.is_counter);
   }
 
+  auto set_parent_id = [&](TrackId id) {
+    if (parent_resolved_track) {
+      auto rr = context_->storage->mutable_track_table()->FindById(id);
+      PERFETTO_CHECK(rr);
+      rr->set_parent_id(parent_resolved_track->track_id());
+    }
+  };
+
   if (parent_resolved_track) {
     switch (parent_resolved_track->scope()) {
-      case ResolvedDescriptorTrack::Scope::kThread:
+      case ResolvedDescriptorTrack::Scope::kThread: {
         // If parent is a thread track, create another thread-associated track.
+        TrackId id;
+        if (reservation.is_counter) {
+          id = context_->track_tracker->InternTrack(
+              kThreadCounterTrackBlueprint,
+              tracks::Dimensions(parent_resolved_track->utid(),
+                                 static_cast<int64_t>(uuid)),
+              tracks::DynamicName(reservation.name), args_fn_non_root,
+              tracks::DynamicUnit(reservation.counter_details->unit));
+        } else {
+          id = context_->track_tracker->InternTrack(
+              kThreadTrackBlueprint,
+              tracks::Dimensions(parent_resolved_track->utid(),
+                                 static_cast<int64_t>(uuid)),
+              tracks::DynamicName(reservation.name), args_fn_non_root);
+        }
+        // If the parent is the default thread scoped track, promote this track
+        // to also be a root thread level track: this is because the default
+        // thread scoped track is *not* owned by track_event and so we cannot
+        // make ourselves a child of it without making the semantics very
+        // strange.
+        if (!parent_resolved_track->is_default_thead_slice_track()) {
+          set_parent_id(id);
+        }
         return ResolvedDescriptorTrack::Thread(
-            parent_resolved_track->utid(), reservation.is_counter,
-            false /* is_root*/, parent_resolved_track->use_separate_track());
-      case ResolvedDescriptorTrack::Scope::kProcess:
+            id, parent_resolved_track->utid(), reservation.is_counter, false);
+      }
+      case ResolvedDescriptorTrack::Scope::kProcess: {
         // If parent is a process track, create another process-associated
         // track.
-        return ResolvedDescriptorTrack::Process(parent_resolved_track->upid(),
-                                                reservation.is_counter,
-                                                false /* is_root*/);
+        StringId translated_name =
+            context_->process_track_translation_table->TranslateName(
+                reservation.name);
+        TrackId id;
+        if (reservation.is_counter) {
+          id = context_->track_tracker->InternTrack(
+              kProcessCounterTrackBlueprint,
+              tracks::Dimensions(parent_resolved_track->upid(),
+                                 static_cast<int64_t>(uuid)),
+              tracks::DynamicName(translated_name), args_fn_non_root,
+              tracks::DynamicUnit(reservation.counter_details->unit));
+        } else {
+          id = context_->track_tracker->InternTrack(
+              kProcessTrackBlueprint,
+              tracks::Dimensions(parent_resolved_track->upid(),
+                                 static_cast<int64_t>(uuid)),
+              tracks::DynamicName(translated_name), args_fn_non_root);
+        }
+        set_parent_id(id);
+        return ResolvedDescriptorTrack::Process(
+            id, parent_resolved_track->upid(), reservation.is_counter);
+      }
       case ResolvedDescriptorTrack::Scope::kGlobal:
         break;
     }
   }
 
-  // Otherwise create a global track.
-
-  // The global track with no uuid is the default global track (e.g. for
-  // global instant events). Any other global tracks are considered children
-  // of the default track.
-  bool is_root_in_scope = !parent_resolved_track;
-  if (!parent_resolved_track && uuid) {
-    // Detect loops where the default track has a parent that itself is a
-    // global track (and thus should be parent of the default track).
-    if (descendent_uuids &&
-        std::find(descendent_uuids->begin(), descendent_uuids->end(),
-                  kDefaultDescriptorTrackUuid) != descendent_uuids->end()) {
-      PERFETTO_ELOG(
-          "Loop detected in parent_track_uuid hierarchy at track %" PRIu64
-          " with parent %" PRIu64,
-          uuid, kDefaultDescriptorTrackUuid);
-      return std::nullopt;
-    }
-
-    // This track will be implicitly a child of the default global track.
-    is_root_in_scope = false;
+  // root_in_scope only matters for legacy JSON export. This is somewhat related
+  // but intentionally distinct from our handling of parent_id relationships.
+  bool is_root_in_scope = uuid == kDefaultDescriptorTrackUuid;
+  TrackId id;
+  if (reservation.is_counter) {
+    id = context_->track_tracker->InternTrack(
+        kGlobalCounterTrackBlueprint,
+        tracks::Dimensions(static_cast<int64_t>(uuid)),
+        tracks::DynamicName(reservation.name),
+        is_root_in_scope ? args_fn_root : args_fn_non_root,
+        tracks::DynamicUnit(reservation.counter_details->unit));
+  } else {
+    id = context_->track_tracker->InternTrack(
+        kGlobalTrackBlueprint, tracks::Dimensions(static_cast<int64_t>(uuid)),
+        tracks::DynamicName(reservation.name),
+        is_root_in_scope ? args_fn_root : args_fn_non_root);
   }
-  return ResolvedDescriptorTrack::Global(reservation.is_counter,
-                                         is_root_in_scope);
-}
-
-TrackId TrackEventTracker::GetOrCreateDefaultDescriptorTrack() {
-  // If the default track was already reserved (e.g. because a producer emitted
-  // a descriptor for it) or created, resolve and return it.
-  std::optional<TrackId> track_id =
-      GetDescriptorTrack(kDefaultDescriptorTrackUuid);
-  if (track_id)
-    return *track_id;
-
-  // Otherwise reserve a new track and resolve it.
-  DescriptorTrackReservation r;
-  r.parent_uuid = 0;
-  r.name = default_descriptor_track_name_;
-  ReserveDescriptorTrack(kDefaultDescriptorTrackUuid, r);
-  return *GetDescriptorTrack(kDefaultDescriptorTrackUuid);
+  set_parent_id(id);
+  return ResolvedDescriptorTrack::Global(id, reservation.is_counter);
 }
 
 std::optional<double> TrackEventTracker::ConvertToAbsoluteCounterValue(
     uint64_t counter_track_uuid,
     uint32_t packet_sequence_id,
     double value) {
-  auto reservation_it = reserved_descriptor_tracks_.find(counter_track_uuid);
-  if (reservation_it == reserved_descriptor_tracks_.end()) {
+  auto* reservation_ptr = reserved_descriptor_tracks_.Find(counter_track_uuid);
+  if (!reservation_ptr) {
     PERFETTO_DLOG("Unknown counter track with uuid %" PRIu64,
                   counter_track_uuid);
     return std::nullopt;
   }
 
-  DescriptorTrackReservation& reservation = reservation_it->second;
+  DescriptorTrackReservation& reservation = *reservation_ptr;
   if (!reservation.is_counter) {
     PERFETTO_DLOG("Track with uuid %" PRIu64 " is not a counter track",
                   counter_track_uuid);
@@ -539,7 +468,6 @@
     c_details.latest_value += value;
     value = c_details.latest_value;
   }
-
   return value;
 }
 
@@ -548,8 +476,8 @@
   // of packet sequences, incremental state clearing at O(trace second), and
   // total number of tracks in O(thousands), a linear scan through all tracks
   // here might not be fast enough.
-  for (auto& entry : reserved_descriptor_tracks_) {
-    DescriptorTrackReservation& reservation = entry.second;
+  for (auto it = reserved_descriptor_tracks_.GetIterator(); it; ++it) {
+    DescriptorTrackReservation& reservation = it.value();
     // Only consider incremental counter tracks for current sequence.
     if (!reservation.is_counter || !reservation.counter_details ||
         !reservation.counter_details->is_incremental ||
@@ -569,16 +497,22 @@
     uint64_t uuid,
     std::optional<uint32_t> packet_sequence_id,
     const DescriptorTrackReservation& reservation,
-    const ResolvedDescriptorTrack& track,
+    bool is_root_in_scope,
     ArgsTracker::BoundInserter& args) {
   args.AddArg(source_key_, Variadic::String(descriptor_source_))
       .AddArg(source_id_key_, Variadic::Integer(static_cast<int64_t>(uuid)))
-      .AddArg(is_root_in_scope_key_,
-              Variadic::Boolean(track.is_root_in_scope()));
-  if (reservation.counter_details &&
-      !reservation.counter_details->category.is_null())
-    args.AddArg(category_key_,
-                Variadic::String(reservation.counter_details->category));
+      .AddArg(is_root_in_scope_key_, Variadic::Boolean(is_root_in_scope));
+  if (reservation.counter_details) {
+    if (!reservation.counter_details->category.is_null()) {
+      args.AddArg(category_key_,
+                  Variadic::String(reservation.counter_details->category));
+    }
+    if (!reservation.counter_details->builtin_type_str.is_null()) {
+      args.AddArg(
+          builtin_counter_type_key_,
+          Variadic::String(reservation.counter_details->builtin_type_str));
+    }
+  }
   if (packet_sequence_id &&
       sequences_with_first_packet_.find(*packet_sequence_id) !=
           sequences_with_first_packet_.end()) {
@@ -605,39 +539,68 @@
   }
 }
 
+bool TrackEventTracker::IsTrackHierarchyValid(uint64_t uuid) {
+  // Do a basic tree walking algorithm to find if there are duplicate ids or
+  // the path to the root is longer than kMaxAncestors.
+  static constexpr size_t kMaxAncestors = 10;
+  std::array<uint64_t, kMaxAncestors> seen;
+  uint64_t current_uuid = uuid;
+  for (uint32_t i = 0; i < kMaxAncestors; ++i) {
+    if (current_uuid == 0) {
+      return true;
+    }
+    for (uint32_t j = 0; j < i; ++j) {
+      if (current_uuid == seen[j]) {
+        PERFETTO_ELOG("Loop detected in hierarchy for track %" PRIu64, uuid);
+        return false;
+      }
+    }
+    auto* reservation_ptr = reserved_descriptor_tracks_.Find(current_uuid);
+    if (!reservation_ptr) {
+      PERFETTO_ELOG("Missing uuid in hierarchy for track %" PRIu64, uuid);
+      return false;
+    }
+    seen[i] = current_uuid;
+    current_uuid = reservation_ptr->parent_uuid;
+  }
+  PERFETTO_ELOG("Too many ancestors in hierarchy for track %" PRIu64, uuid);
+  return false;
+}
+
 TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Process(UniquePid upid,
-                                                    bool is_counter,
-                                                    bool is_root) {
+TrackEventTracker::ResolvedDescriptorTrack::Process(TrackId track_id,
+                                                    UniquePid upid,
+                                                    bool is_counter) {
   ResolvedDescriptorTrack track;
+  track.track_id_ = track_id;
   track.scope_ = Scope::kProcess;
   track.is_counter_ = is_counter;
-  track.is_root_in_scope_ = is_root;
   track.upid_ = upid;
   return track;
 }
 
 TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Thread(UniqueTid utid,
-                                                   bool is_counter,
-                                                   bool is_root,
-                                                   bool use_separate_track) {
+TrackEventTracker::ResolvedDescriptorTrack::Thread(
+    TrackId track_id,
+    UniqueTid utid,
+    bool is_counter,
+    bool is_default_thead_slice_track) {
   ResolvedDescriptorTrack track;
+  track.track_id_ = track_id;
   track.scope_ = Scope::kThread;
   track.is_counter_ = is_counter;
-  track.is_root_in_scope_ = is_root;
   track.utid_ = utid;
-  track.use_separate_track_ = use_separate_track;
+  track.is_default_thead_slice_track_ = is_default_thead_slice_track;
   return track;
 }
 
 TrackEventTracker::ResolvedDescriptorTrack
-TrackEventTracker::ResolvedDescriptorTrack::Global(bool is_counter,
-                                                   bool is_root) {
+TrackEventTracker::ResolvedDescriptorTrack::Global(TrackId track_id,
+                                                   bool is_counter) {
   ResolvedDescriptorTrack track;
+  track.track_id_ = track_id;
   track.scope_ = Scope::kGlobal;
   track.is_counter_ = is_counter;
-  track.is_root_in_scope_ = is_root;
   return track;
 }
 
diff --git a/src/trace_processor/importers/proto/track_event_tracker.h b/src/trace_processor/importers/proto/track_event_tracker.h
index 7848eac..c035a8d 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.h
+++ b/src/trace_processor/importers/proto/track_event_tracker.h
@@ -18,13 +18,13 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
 
 #include <cstdint>
-#include <map>
 #include <optional>
 #include <tuple>
 #include <unordered_set>
-#include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -34,6 +34,8 @@
 // Tracks and stores tracks based on track types, ids and scopes.
 class TrackEventTracker {
  public:
+  static constexpr uint64_t kDefaultDescriptorTrackUuid = 0u;
+
   // Data from TrackDescriptor proto used to reserve a track before interning it
   // with |TrackTracker|.
   struct DescriptorTrackReservation {
@@ -51,19 +53,20 @@
       uint32_t packet_sequence_id = 0;
       double latest_value = 0;
       StringId unit = kNullStringId;
+      StringId builtin_type_str;
 
-      bool operator==(const CounterDetails& o) const {
+      bool IsForSameTrack(const CounterDetails& o) const {
         return std::tie(category, unit_multiplier, is_incremental,
-                        packet_sequence_id, latest_value) ==
+                        packet_sequence_id, builtin_type_str) ==
                std::tie(o.category, o.unit_multiplier, o.is_incremental,
-                        o.packet_sequence_id, o.latest_value);
+                        o.packet_sequence_id, o.builtin_type_str);
       }
     };
 
     uint64_t parent_uuid = 0;
     std::optional<uint32_t> pid;
     std::optional<uint32_t> tid;
-    int64_t min_timestamp = 0;  // only set if |pid| and/or |tid| is set.
+    int64_t min_timestamp = 0;
     StringId name = kNullStringId;
     bool use_separate_track = false;
     bool is_counter = false;
@@ -78,11 +81,16 @@
     // Whether |other| is a valid descriptor for this track reservation. A track
     // should always remain nested underneath its original parent.
     bool IsForSameTrack(const DescriptorTrackReservation& other) {
-      // Note that |min_timestamp|, |latest_value|, and |name| are ignored for
-      // this comparison.
-      return std::tie(parent_uuid, pid, tid, is_counter, counter_details) ==
-             std::tie(other.parent_uuid, other.pid, other.tid, other.is_counter,
-                      other.counter_details);
+      if (counter_details.has_value() != other.counter_details.has_value()) {
+        return false;
+      }
+      if (counter_details &&
+          !counter_details->IsForSameTrack(*other.counter_details)) {
+        return false;
+      }
+      return std::tie(parent_uuid, pid, tid, is_counter) ==
+             std::tie(other.parent_uuid, other.pid, other.tid,
+                      other.is_counter);
     }
   };
   explicit TrackEventTracker(TraceProcessorContext*);
@@ -104,12 +112,16 @@
   // the |uuid|. If the track is a child track and doesn't have a name yet,
   // updates the track's name to event_name. Returns std::nullopt if no track
   // for a descriptor with this |uuid| has been reserved.
-  // TODO(lalitm): this method needs to be split up and moved back to
-  // TrackTracker.
   std::optional<TrackId> GetDescriptorTrack(
       uint64_t uuid,
       StringId event_name = kNullStringId,
-      std::optional<uint32_t> packet_sequence_id = std::nullopt);
+      std::optional<uint32_t> packet_sequence_id = std::nullopt) {
+    auto res = GetDescriptorTrackImpl(uuid, event_name, packet_sequence_id);
+    if (!res) {
+      return std::nullopt;
+    }
+    return res->track_id();
+  }
 
   // Converts the given counter value to an absolute value in the unit of the
   // counter, applying incremental delta encoding or unit multipliers as
@@ -121,11 +133,6 @@
       uint32_t packet_sequence_id,
       double value);
 
-  // Returns the ID of the implicit trace-global default TrackDescriptor track.
-  // TODO(lalitm): this method needs to be moved back to TrackTracker once
-  // GetDescriptorTrack is moved back.
-  TrackId GetOrCreateDefaultDescriptorTrack();
-
   // Called by ProtoTraceReader whenever incremental state is cleared on a
   // packet sequence. Resets counter values for any incremental counters of
   // the sequence identified by |packet_sequence_id|.
@@ -133,14 +140,14 @@
 
   void OnFirstPacketOnSequence(uint32_t packet_sequence_id);
 
-  void SetRangeOfInterestStartUs(int64_t range_of_interest_start_us) {
-    range_of_interest_start_us_ = range_of_interest_start_us;
-  }
-
   std::optional<int64_t> range_of_interest_start_us() const {
     return range_of_interest_start_us_;
   }
 
+  void set_range_of_interest_us(int64_t range_of_interest_start_us) {
+    range_of_interest_start_us_ = range_of_interest_start_us;
+  }
+
  private:
   class ResolvedDescriptorTrack {
    public:
@@ -150,77 +157,71 @@
       kGlobal,
     };
 
-    static ResolvedDescriptorTrack Process(UniquePid upid,
-                                           bool is_counter,
-                                           bool is_root);
-    static ResolvedDescriptorTrack Thread(UniqueTid utid,
+    static ResolvedDescriptorTrack Process(TrackId,
+                                           UniquePid upid,
+                                           bool is_counter);
+    static ResolvedDescriptorTrack Thread(TrackId,
+                                          UniqueTid utid,
                                           bool is_counter,
-                                          bool is_root,
-                                          bool use_separate_track);
-    static ResolvedDescriptorTrack Global(bool is_counter, bool is_root);
+                                          bool is_default_thead_slice_track);
+    static ResolvedDescriptorTrack Global(TrackId, bool is_counter);
 
+    TrackId track_id() const { return track_id_; }
     Scope scope() const { return scope_; }
     bool is_counter() const { return is_counter_; }
     UniqueTid utid() const {
       PERFETTO_DCHECK(scope() == Scope::kThread);
       return utid_;
     }
+    bool is_default_thead_slice_track() const {
+      PERFETTO_DCHECK(scope() == Scope::kThread);
+      return is_default_thead_slice_track_;
+    }
     UniquePid upid() const {
       PERFETTO_DCHECK(scope() == Scope::kProcess);
       return upid_;
     }
-    UniqueTid is_root_in_scope() const { return is_root_in_scope_; }
-    bool use_separate_track() const { return use_separate_track_; }
 
    private:
+    TrackId track_id_;
     Scope scope_;
     bool is_counter_;
-    bool is_root_in_scope_;
-    bool use_separate_track_;
 
     // Only set when |scope| == |Scope::kThread|.
     UniqueTid utid_;
+    bool is_default_thead_slice_track_ = false;
 
     // Only set when |scope| == |Scope::kProcess|.
     UniquePid upid_;
   };
 
-  std::optional<TrackId> GetDescriptorTrackImpl(
+  std::optional<ResolvedDescriptorTrack> GetDescriptorTrackImpl(
       uint64_t uuid,
-      std::optional<uint32_t> packet_sequence_id = std::nullopt);
-  TrackId CreateTrackFromResolved(uint64_t uuid,
-                                  std::optional<uint32_t> packet_sequence_id,
-                                  const DescriptorTrackReservation&,
-                                  const ResolvedDescriptorTrack&);
-  std::optional<ResolvedDescriptorTrack> ResolveDescriptorTrack(
+      StringId event_name,
+      std::optional<uint32_t> packet_sequence_id);
+
+  ResolvedDescriptorTrack ResolveDescriptorTrack(
       uint64_t uuid,
-      std::vector<uint64_t>* descendent_uuids);
-  std::optional<ResolvedDescriptorTrack> ResolveDescriptorTrackImpl(
-      uint64_t uuid,
-      const DescriptorTrackReservation&,
-      std::vector<uint64_t>* descendent_uuids);
+      const DescriptorTrackReservation& reservation,
+      std::optional<uint32_t> packet_sequence_id);
+
+  bool IsTrackHierarchyValid(uint64_t uuid);
 
   void AddTrackArgs(uint64_t uuid,
                     std::optional<uint32_t> packet_sequence_id,
                     const DescriptorTrackReservation&,
-                    const ResolvedDescriptorTrack&,
+                    bool,
                     ArgsTracker::BoundInserter&);
 
-  static constexpr uint64_t kDefaultDescriptorTrackUuid = 0u;
-
-  std::map<UniqueTid, TrackId> thread_tracks_;
-  std::map<UniquePid, TrackId> process_tracks_;
-
-  std::map<uint64_t /* uuid */, DescriptorTrackReservation>
+  base::FlatHashMap<uint64_t /* uuid */, DescriptorTrackReservation>
       reserved_descriptor_tracks_;
-  std::map<uint64_t /* uuid */, ResolvedDescriptorTrack>
+  base::FlatHashMap<uint64_t /* uuid */, ResolvedDescriptorTrack>
       resolved_descriptor_tracks_;
-  std::map<uint64_t /* uuid */, TrackId> descriptor_tracks_;
 
   // Stores the descriptor uuid used for the primary process/thread track
   // for the given upid / utid. Used for pid/tid reuse detection.
-  std::map<UniquePid, uint64_t /*uuid*/> descriptor_uuids_by_upid_;
-  std::map<UniqueTid, uint64_t /*uuid*/> descriptor_uuids_by_utid_;
+  base::FlatHashMap<UniquePid, uint64_t /*uuid*/> descriptor_uuids_by_upid_;
+  base::FlatHashMap<UniqueTid, uint64_t /*uuid*/> descriptor_uuids_by_utid_;
 
   std::unordered_set<uint32_t> sequences_with_first_packet_;
 
@@ -228,19 +229,17 @@
   const StringId source_id_key_;
   const StringId is_root_in_scope_key_;
   const StringId category_key_;
+  const StringId builtin_counter_type_key_;
   const StringId has_first_packet_on_sequence_key_id_;
   const StringId child_ordering_key_;
   const StringId explicit_id_;
   const StringId lexicographic_id_;
   const StringId chronological_id_;
   const StringId sibling_order_rank_key_;
-
   const StringId descriptor_source_;
-
   const StringId default_descriptor_track_name_;
 
   std::optional<int64_t> range_of_interest_start_us_;
-
   TraceProcessorContext* const context_;
 };
 
diff --git a/src/trace_processor/importers/proto/v8_module.cc b/src/trace_processor/importers/proto/v8_module.cc
index 88d8f53..600e2b1 100644
--- a/src/trace_processor/importers/proto/v8_module.cc
+++ b/src/trace_processor/importers/proto/v8_module.cc
@@ -240,7 +240,7 @@
 }
 
 void V8Module::ParseV8CodeMove(protozero::ConstBytes bytes,
-                               int64_t,
+                               int64_t ts,
                                const TracePacketData& data) {
   V8SequenceState& state =
       *data.sequence_state->GetCustomState<V8SequenceState>();
@@ -252,7 +252,13 @@
     return;
   }
 
-  // TODO(carlscab): Implement
+  std::optional<UniqueTid> utid =
+      GetUtid(*data.sequence_state, *isolate_id, v8_code_move);
+  if (!utid) {
+    return;
+  }
+
+  v8_tracker_->MoveCode(ts, *utid, *isolate_id, v8_code_move);
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/v8_tracker.cc b/src/trace_processor/importers/proto/v8_tracker.cc
index d60bef1..2866638 100644
--- a/src/trace_processor/importers/proto/v8_tracker.cc
+++ b/src/trace_processor/importers/proto/v8_tracker.cc
@@ -51,6 +51,7 @@
 using ::perfetto::protos::pbzero::InternedV8JsFunction;
 using ::perfetto::protos::pbzero::InternedV8JsScript;
 using ::perfetto::protos::pbzero::InternedV8WasmScript;
+using ::perfetto::protos::pbzero::V8CodeMove;
 using ::perfetto::protos::pbzero::V8InternalCode;
 using ::perfetto::protos::pbzero::V8JsCode;
 using ::perfetto::protos::pbzero::V8RegExpCode;
@@ -341,10 +342,13 @@
   row.is_toplevel = function.is_toplevel();
   row.kind =
       context_->storage->InternString(JsFunctionKindToString(function.kind()));
-  // TODO(carlscab): Line and column are hard. Offset is in bytes, line and
-  // column are in characters and we potentially have a multi byte encoding
-  // (UTF16). Good luck!
-  if (function.has_byte_offset()) {
+  if (function.has_line() && function.has_column()) {
+    row.line = function.line();
+    row.col = function.column();
+  } else if (function.has_byte_offset()) {
+    // TODO(carlscab): Line and column are hard. Offset is in bytes, line and
+    // column are in characters and we potentially have a multi byte encoding
+    // (UTF16). Good luck!
     row.line = 1;
     row.col = function.byte_offset();
   }
@@ -520,6 +524,24 @@
       {jit_code_id, isolate_id, pattern});
 }
 
+void V8Tracker::MoveCode(int64_t timestamp,
+                         UniqueTid utid,
+                         IsolateId isolate_id,
+                         const V8CodeMove::Decoder& code) {
+  if (!code.has_from_instruction_start_address())
+    return;
+
+  const AddressRange code_range = AddressRange::FromStartAndSize(
+      code.from_instruction_start_address(), code.instruction_size_bytes());
+  JitCache* const jit_cache = FindJitCache(isolate_id, code_range);
+  if (!jit_cache) {
+    return;
+  }
+
+  jit_cache->MoveCode(timestamp, utid, code.from_instruction_start_address(),
+                      code.to_instruction_start_address());
+}
+
 StringId V8Tracker::InternV8String(const V8String::Decoder& v8_string) {
   auto& storage = *context_->storage;
   if (v8_string.has_latin1()) {
diff --git a/src/trace_processor/importers/proto/v8_tracker.h b/src/trace_processor/importers/proto/v8_tracker.h
index 0abcc30..03176d6 100644
--- a/src/trace_processor/importers/proto/v8_tracker.h
+++ b/src/trace_processor/importers/proto/v8_tracker.h
@@ -87,6 +87,11 @@
                      IsolateId v8_isolate_id,
                      const protos::pbzero::V8RegExpCode::Decoder& code);
 
+  void MoveCode(int64_t timestamp,
+                UniqueTid utid,
+                IsolateId v8_isolate_id,
+                const protos::pbzero::V8CodeMove::Decoder& code_move);
+
  private:
   struct JsFunctionHash {
     size_t operator()(const tables::V8JsFunctionTable::Row& v) const {
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.cc b/src/trace_processor/importers/proto/winscope/winscope_module.cc
index a25a05d..c4ddab4 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.cc
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.cc
@@ -15,23 +15,35 @@
  */
 
 #include "src/trace_processor/importers/proto/winscope/winscope_module.h"
+
+#include <cstdint>
+
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/base64.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "protos/perfetto/trace/android/winscope_extensions.pbzero.h"
 #include "protos/perfetto/trace/android/winscope_extensions_impl.pbzero.h"
+#include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/proto/args_parser.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/winscope/viewcapture_args_parser.h"
 #include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/tables/winscope_tables_py.h"
 #include "src/trace_processor/util/winscope_proto_mapping.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 using perfetto::protos::pbzero::TracePacket;
 using perfetto::protos::pbzero::WinscopeExtensionsImpl;
 
 WinscopeModule::WinscopeModule(TraceProcessorContext* context)
     : context_{context},
-      args_parser_{*context->descriptor_pool_.get()},
+      args_parser_{*context->descriptor_pool_},
       surfaceflinger_layers_parser_(context),
       surfaceflinger_transactions_parser_(context),
       shell_transitions_parser_(context),
@@ -56,7 +68,6 @@
     int64_t /*packet_timestamp*/,
     RefPtr<PacketSequenceStateGeneration> /*state*/,
     uint32_t field_id) {
-
   switch (field_id) {
     case TracePacket::kProtologViewerConfigFieldNumber:
       protolog_parser_.ParseAndAddViewerConfigToMessageDecoder(
@@ -144,7 +155,7 @@
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
-  ArgsParser writer(timestamp, inserter, *context_->storage.get());
+  ArgsParser writer(timestamp, inserter, *context_->storage);
   base::Status status =
       args_parser_.ParseMessage(blob,
                                 *util::winscope_proto_mapping::GetProtoName(
@@ -170,7 +181,7 @@
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
-  ArgsParser writer(timestamp, inserter, *context_->storage.get());
+  ArgsParser writer(timestamp, inserter, *context_->storage);
   base::Status status = args_parser_.ParseMessage(
       blob,
       *util::winscope_proto_mapping::GetProtoName(
@@ -194,7 +205,7 @@
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
-  ArgsParser writer(timestamp, inserter, *context_->storage.get());
+  ArgsParser writer(timestamp, inserter, *context_->storage);
   base::Status status =
       args_parser_.ParseMessage(blob,
                                 *util::winscope_proto_mapping::GetProtoName(
@@ -219,7 +230,7 @@
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
-  ViewCaptureArgsParser writer(timestamp, inserter, *context_->storage.get(),
+  ViewCaptureArgsParser writer(timestamp, inserter, *context_->storage,
                                sequence_state);
   base::Status status =
       args_parser_.ParseMessage(blob,
@@ -242,7 +253,7 @@
 
   ArgsTracker tracker(context_);
   auto inserter = tracker.AddArgsTo(rowId);
-  ArgsParser writer(timestamp, inserter, *context_->storage.get());
+  ArgsParser writer(timestamp, inserter, *context_->storage);
   base::Status status =
       args_parser_.ParseMessage(blob,
                                 *util::winscope_proto_mapping::GetProtoName(
@@ -254,5 +265,4 @@
   }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.cc b/src/trace_processor/importers/systrace/systrace_line_parser.cc
index 74980e7..2906b1f 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.cc
@@ -56,7 +56,7 @@
       waker_utid_id_(ctx->storage->InternString("waker_utid")),
       unknown_thread_name_id_(ctx->storage->InternString("<...>")) {}
 
-util::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
+base::Status SystraceLineParser::ParseLine(const SystraceLine& line) {
   const StringId line_task_id{
       context_->storage->InternString(base::StringView(line.task))};
   auto utid = context_->process_tracker->UpdateThreadName(
@@ -109,7 +109,7 @@
 
     if (!(prev_pid.has_value() && prev_prio.has_value() &&
           next_pid.has_value() && next_prio.has_value())) {
-      return util::Status("Could not parse sched_switch");
+      return base::Status("Could not parse sched_switch");
     }
 
     FtraceSchedEventTracker::GetOrCreate(context_)->PushSchedSwitch(
@@ -123,7 +123,7 @@
     auto comm = args["comm"];
     std::optional<uint32_t> wakee_pid = base::StringToUInt32(args["pid"]);
     if (!wakee_pid.has_value()) {
-      return util::Status("Could not convert wakee_pid");
+      return base::Status("Could not convert wakee_pid");
     }
 
     StringId name_id = context_->storage->InternString(base::StringView(comm));
@@ -137,10 +137,10 @@
     std::optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
     std::optional<double> new_state = base::StringToDouble(args["state"]);
     if (!event_cpu.has_value()) {
-      return util::Status("Could not convert event cpu");
+      return base::Status("Could not convert event cpu");
     }
     if (!event_cpu.has_value()) {
-      return util::Status("Could not convert state");
+      return base::Status("Could not convert state");
     }
 
     TrackId track = context_->track_tracker->InternTrack(
@@ -150,10 +150,10 @@
     std::optional<uint32_t> event_cpu = base::StringToUInt32(args["cpu_id"]);
     std::optional<double> new_state = base::StringToDouble(args["state"]);
     if (!event_cpu.has_value()) {
-      return util::Status("Could not convert event cpu");
+      return base::Status("Could not convert event cpu");
     }
     if (!event_cpu.has_value()) {
-      return util::Status("Could not convert state");
+      return base::Status("Could not convert state");
     }
 
     TrackId track = context_->track_tracker->InternTrack(
@@ -171,16 +171,16 @@
     std::string code_str = args["code"] + " Java Layer Dependent";
     StringId code = context_->storage->InternString(base::StringView(code_str));
     if (!dest_tgid.has_value()) {
-      return util::Status("Could not convert dest_tgid");
+      return base::Status("Could not convert dest_tgid");
     }
     if (!dest_tid.has_value()) {
-      return util::Status("Could not convert dest_tid");
+      return base::Status("Could not convert dest_tid");
     }
     if (!id.has_value()) {
-      return util::Status("Could not convert transaction id");
+      return base::Status("Could not convert transaction id");
     }
     if (!dest_node.has_value()) {
-      return util::Status("Could not covert dest node");
+      return base::Status("Could not covert dest node");
     }
     BinderTracker::GetOrCreate(context_)->Transaction(
         line.ts, line.pid, id.value(), dest_node.value(), dest_tgid.value(),
@@ -188,21 +188,21 @@
   } else if (line.event_name == "binder_transaction_received") {
     auto id = base::StringToInt32(args["transaction"]);
     if (!id.has_value()) {
-      return util::Status("Could not convert transaction id");
+      return base::Status("Could not convert transaction id");
     }
     BinderTracker::GetOrCreate(context_)->TransactionReceived(line.ts, line.pid,
                                                               id.value());
   } else if (line.event_name == "binder_command") {
     auto id = base::StringToUInt32(args["cmd"], 0);
     if (!id.has_value()) {
-      return util::Status("Could not convert cmd ");
+      return base::Status("Could not convert cmd ");
     }
     BinderTracker::GetOrCreate(context_)->CommandToKernel(line.ts, line.pid,
                                                           id.value());
   } else if (line.event_name == "binder_return") {
     auto id = base::StringToUInt32(args["cmd"], 0);
     if (!id.has_value()) {
-      return util::Status("Could not convert cmd");
+      return base::Status("Could not convert cmd");
     }
     BinderTracker::GetOrCreate(context_)->ReturnFromKernel(line.ts, line.pid,
                                                            id.value());
@@ -216,17 +216,17 @@
     auto data_size = base::StringToUInt64(args["data_size"]);
     auto offsets_size = base::StringToUInt64(args["offsets_size"]);
     if (!data_size.has_value()) {
-      return util::Status("Could not convert data size");
+      return base::Status("Could not convert data size");
     }
     if (!offsets_size.has_value()) {
-      return util::Status("Could not convert offsets size");
+      return base::Status("Could not convert offsets size");
     }
     BinderTracker::GetOrCreate(context_)->TransactionAllocBuf(
         line.ts, line.pid, data_size.value(), offsets_size.value());
   } else if (line.event_name == "clock_set_rate") {
     auto rate = base::StringToUInt32(args["state"]);
     if (!rate.has_value()) {
-      return util::Status("Could not convert state");
+      return base::Status("Could not convert state");
     }
     TrackId track = context_->track_tracker->InternTrack(
         tracks::kClockFrequencyBlueprint,
@@ -236,7 +236,7 @@
              line.event_name == "clock_disable") {
     auto rate = base::StringToUInt32(args["state"]);
     if (!rate.has_value()) {
-      return util::Status("Could not convert state");
+      return base::Status("Could not convert state");
     }
     TrackId track = context_->track_tracker->InternTrack(
         tracks::kClockStateBlueprint,
@@ -257,7 +257,7 @@
         tracks::Dimensions(base::StringView(args["thermal_zone"])));
     auto temp = base::StringToInt32(args["temp"]);
     if (!temp.has_value()) {
-      return util::Status("Could not convert temp");
+      return base::Status("Could not convert temp");
     }
     context_->event_tracker->PushCounter(line.ts, temp.value(), track);
   } else if (line.event_name == "cdev_update") {
@@ -266,18 +266,18 @@
         tracks::Dimensions(base::StringView(args["type"])));
     auto target = base::StringToDouble(args["target"]);
     if (!target.has_value()) {
-      return util::Status("Could not convert target");
+      return base::Status("Could not convert target");
     }
     context_->event_tracker->PushCounter(line.ts, target.value(), track);
   } else if (line.event_name == "sched_blocked_reason") {
     auto wakee_pid = base::StringToUInt32(args["pid"]);
     if (!wakee_pid.has_value()) {
-      return util::Status("sched_blocked_reason: could not parse wakee_pid");
+      return base::Status("sched_blocked_reason: could not parse wakee_pid");
     }
     auto wakee_utid = context_->process_tracker->GetOrCreateThread(*wakee_pid);
     auto io_wait = base::StringToInt32(args["iowait"]);
     if (!io_wait.has_value()) {
-      return util::Status("sched_blocked_reason: could not parse io_wait");
+      return base::Status("sched_blocked_reason: could not parse io_wait");
     }
     StringId blocked_function =
         context_->storage->InternString(base::StringView(args["caller"]));
@@ -290,10 +290,10 @@
     auto mm_id = base::StringToInt64(args["mm_id"]);
     auto opt_curr = base::StringToUInt32(args["curr"]);
     if (!size.has_value()) {
-      return util::Status("rss_stat: could not parse size");
+      return base::Status("rss_stat: could not parse size");
     }
     if (!member.has_value()) {
-      return util::Status("rss_stat: could not parse member");
+      return base::Status("rss_stat: could not parse member");
     }
     std::optional<bool> curr;
     if (!opt_curr.has_value()) {
@@ -303,7 +303,7 @@
                                    mm_id);
   }
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_parser.h b/src/trace_processor/importers/systrace/systrace_line_parser.h
index 4ff2b83..bf53d94 100644
--- a/src/trace_processor/importers/systrace/systrace_line_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_line_parser.h
@@ -31,7 +31,7 @@
  public:
   explicit SystraceLineParser(TraceProcessorContext*);
 
-  util::Status ParseLine(const SystraceLine&);
+  base::Status ParseLine(const SystraceLine&);
 
  private:
   TraceProcessorContext* const context_;
diff --git a/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc b/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
index 054bc3e..6daf6de 100644
--- a/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
+++ b/src/trace_processor/importers/systrace/systrace_line_tokenizer.cc
@@ -46,7 +46,7 @@
 
 // TODO(hjd): This should be more robust to being passed random input.
 // This can happen if we mess up detecting a gzip trace for example.
-util::Status SystraceLineTokenizer::Tokenize(const std::string& buffer,
+base::Status SystraceLineTokenizer::Tokenize(const std::string& buffer,
                                              SystraceLine* line) {
   // An example line from buffer looks something like the following:
   // kworker/u16:1-77    (   77) [004] ....   316.196720: 0:
@@ -65,7 +65,7 @@
   std::smatch matches;
   bool matched = std::regex_search(buffer, matches, line_matcher_);
   if (!matched) {
-    return util::ErrStatus("Not a known systrace event format (line: %s)",
+    return base::ErrStatus("Not a known systrace event format (line: %s)",
                            buffer.c_str());
   }
 
@@ -80,23 +80,23 @@
 
   std::optional<uint32_t> maybe_pid = base::StringToUInt32(pid_str);
   if (!maybe_pid.has_value()) {
-    return util::Status("Could not convert pid " + pid_str);
+    return base::Status("Could not convert pid " + pid_str);
   }
   line->pid = maybe_pid.value();
 
   std::optional<uint32_t> maybe_cpu = base::StringToUInt32(cpu_str);
   if (!maybe_cpu.has_value()) {
-    return util::Status("Could not convert cpu " + cpu_str);
+    return base::Status("Could not convert cpu " + cpu_str);
   }
   line->cpu = maybe_cpu.value();
 
   std::optional<double> maybe_ts = base::StringToDouble(ts_str);
   if (!maybe_ts.has_value()) {
-    return util::Status("Could not convert ts");
+    return base::Status("Could not convert ts");
   }
   line->ts = static_cast<int64_t>(maybe_ts.value() * 1e9);
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/systrace/systrace_line_tokenizer.h b/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
index 10c4a28..b861c96 100644
--- a/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
+++ b/src/trace_processor/importers/systrace/systrace_line_tokenizer.h
@@ -30,7 +30,7 @@
  public:
   SystraceLineTokenizer();
 
-  util::Status Tokenize(const std::string& line, SystraceLine*);
+  base::Status Tokenize(const std::string& line, SystraceLine*);
 
  private:
   const std::regex line_matcher_;
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.cc b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
index a28800c..e52f867 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
@@ -66,9 +66,9 @@
     : line_parser_(ctx), ctx_(ctx) {}
 SystraceTraceParser::~SystraceTraceParser() = default;
 
-util::Status SystraceTraceParser::Parse(TraceBlobView blob) {
+base::Status SystraceTraceParser::Parse(TraceBlobView blob) {
   if (state_ == ParseState::kEndOfSystrace)
-    return util::OkStatus();
+    return base::OkStatus();
   partial_buf_.insert(partial_buf_.end(), blob.data(),
                       blob.data() + blob.size());
 
@@ -122,7 +122,7 @@
         break;
       } else if (!base::StartsWith(buffer, "#") && !buffer.empty()) {
         SystraceLine line;
-        util::Status status = line_tokenizer_.Tokenize(buffer, &line);
+        base::Status status = line_tokenizer_.Tokenize(buffer, &line);
         if (status.ok()) {
           line_parser_.ParseLine(std::move(line));
         } else {
@@ -156,7 +156,7 @@
               static_cast<size_t>((buffer.data() + buffer.size()) - cmd_start));
           if (!pid || !ppid) {
             PERFETTO_ELOG("Could not parse line '%s'", buffer.c_str());
-            return util::ErrStatus("Could not parse PROCESS DUMP line");
+            return base::ErrStatus("Could not parse PROCESS DUMP line");
           }
           ctx_->process_tracker->SetProcessMetadata(pid.value(), ppid, name,
                                                     base::StringView());
@@ -177,7 +177,7 @@
               ctx_->storage->mutable_string_pool()->InternString(cmd);
           if (!tid || !tgid) {
             PERFETTO_ELOG("Could not parse line '%s'", buffer.c_str());
-            return util::ErrStatus("Could not parse PROCESS DUMP line");
+            return base::ErrStatus("Could not parse PROCESS DUMP line");
           }
           UniqueTid utid =
               ctx_->process_tracker->UpdateThread(tid.value(), tgid.value());
@@ -198,7 +198,7 @@
   } else {
     partial_buf_.erase(partial_buf_.begin(), start_it);
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 base::Status SystraceTraceParser::NotifyEndOfFile() {
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.h b/src/trace_processor/importers/systrace/systrace_trace_parser.h
index e6b3596..c16d77d 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.h
@@ -35,7 +35,7 @@
   ~SystraceTraceParser() override;
 
   // ChunkedTraceReader implementation.
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   base::Status NotifyEndOfFile() override;
 
  private:
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index 5ad8a64..252cdb1 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -26,6 +26,7 @@
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
+    "android_blocking_calls_cuj_per_frame_metric.sql",
     "android_blocking_calls_unagg.sql",
     "android_boot.sql",
     "android_boot_unagg.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql
new file mode 100644
index 0000000..dd4ce90
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_per_frame_metric.sql
@@ -0,0 +1,205 @@
+--
+-- 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 the base table (`android_jank_cuj`) containing all completed CUJs
+-- found in the trace.
+-- This script will use the `android_jank_cuj_main_thread_frame_boundary`,
+-- containing bounds of frames within jank CUJs.
+SELECT RUN_METRIC('android/android_jank_cuj.sql');
+
+INCLUDE PERFETTO MODULE android.slices;
+INCLUDE PERFETTO MODULE android.binder;
+INCLUDE PERFETTO MODULE android.critical_blocking_calls;
+INCLUDE PERFETTO MODULE android.frames.timeline;
+
+-- TODO(b/296349525): Add this to the perfetto standard library.
+DROP TABLE IF EXISTS android_cujs;
+CREATE TABLE android_cujs AS
+    SELECT
+        cuj_id,
+        cuj.upid,
+        t.utid AS ui_thread,
+        process_name,
+        process_metadata,
+        cuj_name,
+        cuj.layer_id,
+        tb.ts,
+        tb.dur,
+        tb.ts_end,
+        begin_vsync,
+        end_vsync
+    FROM android_jank_cuj_main_thread_cuj_boundary tb
+    JOIN android_jank_cuj cuj USING (cuj_id)
+    JOIN android_jank_cuj_main_thread t USING (cuj_id);
+
+DROP TABLE IF EXISTS _android_frames_layers_with_end_ts;
+CREATE PERFETTO TABLE _android_frames_layers_with_end_ts AS
+    SELECT
+        ts,
+        dur,
+        frame_id,
+        layer_id,
+        upid,
+        ui_thread_utid,
+        (ts + dur) AS ts_end
+    FROM android_frames_layers;
+
+-- While calculating the metric, there are two possibilities for a blocking call:
+-- 1. Blocking call is completely within a frame boundary.
+-- 2. Blocking call crosses the frame boundary into the next frame.
+-- For the first case, the blocking call duration is the 'dur' field value itself. But for the
+-- second case, only the part within the frame is considered.
+DROP TABLE IF EXISTS blocking_calls_per_frame;
+CREATE PERFETTO TABLE blocking_calls_per_frame AS
+SELECT
+    MIN(
+        bc.dur,
+        frame.ts_end - bc.ts,
+        bc.ts_end - frame.ts
+    ) AS dur,
+    MAX(frame.ts, bc.ts) AS ts,
+    bc.upid,
+    bc.name,
+    bc.process_name,
+    bc.utid,
+    frame.frame_id,
+    frame.layer_id
+FROM _android_critical_blocking_calls bc
+JOIN _android_frames_layers_with_end_ts frame
+ON bc.utid = frame.ui_thread_utid
+   -- The following condition to accommodate blocking call crossing frame boundary. The blocking
+   -- call starts in a frame and ends in a frame. It can either be the same frame or a different
+   -- frame.
+WHERE (bc.ts >= frame.ts AND bc.ts <= frame.ts_end) -- Blocking call starts within the frame.
+    OR (bc.ts_end >= frame.ts AND bc.ts_end <= frame.ts_end); -- Blocking call ends within the frame.
+
+-- Table capturing the full and partial frames within a CUJ boundary. This table captures the
+-- layer ID associated with the actual frame too.
+DROP TABLE IF EXISTS frames_in_cuj;
+CREATE PERFETTO TABLE frames_in_cuj AS
+SELECT
+    cuj.cuj_name,
+    cuj.upid,
+    cuj.process_name,
+    frame.layer_id,
+    frame.frame_id,
+    cuj.cuj_id,
+    MAX(frame.ts, cuj.ts) AS frame_ts,
+    MIN(
+        frame.dur,
+        cuj.ts_end - frame.ts,
+        frame.ts_end - cuj.ts
+    ) AS dur
+FROM _android_frames_layers_with_end_ts frame
+JOIN android_cujs cuj
+ON frame.upid = cuj.upid
+   AND frame.layer_id = cuj.layer_id
+   AND frame.ui_thread_utid = cuj.ui_thread
+-- Check whether the frame_id falls within the begin and end vsync of the cuj.
+-- Also check if the frame start or end timestamp falls within the cuj boundary.
+WHERE
+   -- frame withtin cuj vsync boundary
+   frame_id >= begin_vsync AND frame_id <= end_vsync
+   AND (
+      -- frame start within cuj
+      (frame.ts >= cuj.ts AND frame.ts <= cuj.ts_end)
+      OR
+      -- frame end within cuj
+      (frame.ts_end >= cuj.ts AND frame.ts_end <= cuj.ts_end)
+   );
+
+-- Combine the above two tables to get blocking calls within frame within CUJ.
+DROP TABLE IF EXISTS blocking_calls_frame_cuj;
+CREATE PERFETTO TABLE blocking_calls_frame_cuj AS
+SELECT
+    b.upid,
+    b.frame_id,
+    b.layer_id,
+    b.name,
+    frame_cuj.cuj_name,
+    b.ts,
+    b.dur,
+    b.process_name
+FROM frames_in_cuj frame_cuj
+JOIN blocking_calls_per_frame b
+USING (upid, frame_id, layer_id);
+
+-- Calculate the mean/max values for duration and count for blocking calls per frame.
+DROP TABLE IF EXISTS android_blocking_calls_cuj_per_frame_calls;
+CREATE PERFETTO TABLE android_blocking_calls_cuj_per_frame_calls AS
+WITH blocking_calls_aggregate_values AS (
+  -- Aggregate the count and sum for each blocking call by grouping on CUJ name, blocking
+  -- call name and frame ID(vsync).
+  SELECT
+    COUNT(*) AS cnt,
+    SUM(dur) AS total_dur_per_frame_ns,
+    MAX(dur) AS max_dur_per_frame_ns,
+    cuj_name,
+    upid,
+    process_name,
+    name
+  FROM blocking_calls_frame_cuj
+  GROUP BY cuj_name, name, frame_id
+),
+frame_cnt_per_cuj AS (
+  -- Calculate the total number of frames for all CUJs across all instances(eg. multiple
+  -- instances for the same CUJ).
+  SELECT
+    COUNT(*) AS frame_cnt,
+    cuj_name
+  FROM frames_in_cuj
+  GROUP BY cuj_name
+)
+SELECT
+    cast_double!(SUM(cnt)) / frame_cnt AS mean_cnt_per_frame,
+    MAX(cnt) AS max_cnt_per_frame,
+    SUM(total_dur_per_frame_ns) / frame_cnt AS mean_dur_per_frame_ns,
+    MAX(max_dur_per_frame_ns) AS max_dur_per_frame_ns,
+    name,
+    upid,
+    bc.cuj_name,
+    process_name
+FROM blocking_calls_aggregate_values bc
+JOIN frame_cnt_per_cuj fc
+USING(cuj_name)
+GROUP BY bc.cuj_name, name;
+
+DROP VIEW IF EXISTS android_blocking_calls_cuj_per_frame_metric_output;
+CREATE PERFETTO VIEW android_blocking_calls_cuj_per_frame_metric_output AS
+SELECT AndroidCujBlockingCallsPerFrameMetric('cuj', (
+    SELECT RepeatedField(
+        AndroidCujBlockingCallsPerFrameMetric_Cuj(
+            'name', cuj_name,
+            'process', process_metadata,
+            'blocking_calls', (
+                SELECT RepeatedField(
+                    AndroidBlockingCallPerFrame(
+                        'name', b.name,
+                        'max_dur_per_frame_ms', CAST(max_dur_per_frame_ns / 1e6 AS INT),
+                        'max_dur_per_frame_ns', b.max_dur_per_frame_ns,
+                        'mean_dur_per_frame_ms', CAST(mean_dur_per_frame_ns / 1e6 AS INT),
+                        'mean_dur_per_frame_ns', b.mean_dur_per_frame_ns,
+                        'max_cnt_per_frame', CAST(b.max_cnt_per_frame AS INT),
+                        'mean_cnt_per_frame', b.mean_cnt_per_frame
+                    )
+                )
+                FROM android_blocking_calls_cuj_per_frame_calls b
+                WHERE b.cuj_name = cuj.cuj_name and b.upid = cuj.upid
+                GROUP BY b.cuj_name
+            )
+        )
+    )
+    FROM (SELECT DISTINCT cuj_name, upid, process_metadata FROM android_cujs) cuj
+));
diff --git a/src/trace_processor/metrics/sql/android/jank/frames.sql b/src/trace_processor/metrics/sql/android/jank/frames.sql
index 481bb4b..aad4c41 100644
--- a/src/trace_processor/metrics/sql/android/jank/frames.sql
+++ b/src/trace_processor/metrics/sql/android/jank/frames.sql
@@ -56,9 +56,8 @@
   MAX(timeline.layer_name) as frame_layer_name
 FROM android_jank_cuj_vsync_boundary boundary
 JOIN actual_timeline_with_vsync timeline
-  ON boundary.upid = timeline.upid
-    AND vsync >= vsync_min
-    AND vsync <= vsync_max
+  ON vsync >= vsync_min
+     AND vsync <= vsync_max
 LEFT JOIN expected_frame_timeline_slice expected
   ON expected.upid = timeline.upid AND expected.name = timeline.name
 LEFT JOIN vsync_missed_callback missed_callback USING(vsync)
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql b/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
index a340d82..eef18ee 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql
@@ -21,7 +21,7 @@
 CREATE PERFETTO VIEW chrome_event_metadata AS
 WITH metadata (arg_set_id) AS (
   SELECT arg_set_id
-  FROM raw
+  FROM __intrinsic_chrome_raw
   WHERE name = "chrome_event.metadata"
 )
 -- TODO(b/173201788): Once this is fixed, extract all the fields.
diff --git a/src/trace_processor/metrics/sql/experimental/frame_times.sql b/src/trace_processor/metrics/sql/experimental/frame_times.sql
index a3a73c5..9b6f527 100644
--- a/src/trace_processor/metrics/sql/experimental/frame_times.sql
+++ b/src/trace_processor/metrics/sql/experimental/frame_times.sql
@@ -24,7 +24,7 @@
 SELECT
   ts,
   EXTRACT_ARG(arg_set_id, 'legacy_event.phase') AS phase
-FROM raw
+FROM __intrinsic_chrome_raw
 WHERE EXTRACT_ARG(arg_set_id, 'legacy_event.name') = 'SyntheticGestureController::running';
 
 -- Convert pairs of 'S' and 'F' events into slices with ts and dur.
diff --git a/src/trace_processor/metrics/sql/trace_metadata.sql b/src/trace_processor/metrics/sql/trace_metadata.sql
index 1e45573..b90cda9 100644
--- a/src/trace_processor/metrics/sql/trace_metadata.sql
+++ b/src/trace_processor/metrics/sql/trace_metadata.sql
@@ -43,6 +43,10 @@
     FROM track JOIN slice ON track.id = slice.track_id
     WHERE track.name = 'Trace Triggers'
   ),
+  'trace_causal_trigger', (
+      SELECT str_value FROM metadata
+      WHERE name = 'trace_trigger'
+  ),
   'trace_config_pbtxt', (
     SELECT str_value FROM metadata
     WHERE name = 'trace_config_pbtxt'
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index 1c01c17..e6cef14 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -109,7 +109,7 @@
   // the destructors run correctly for non-trivial members of the
   // union.
   using Data =
-      std::variant<int64_t, double, OwnedString, OwnedBytes, nullptr_t>;
+      std::variant<int64_t, double, OwnedString, OwnedBytes, std::nullptr_t>;
 
   StoredSqlValue(SqlValue value) {
     switch (value.type) {
@@ -134,7 +134,7 @@
   }
 
   SqlValue AsSqlValue() {
-    if (std::holds_alternative<nullptr_t>(data)) {
+    if (std::holds_alternative<std::nullptr_t>(data)) {
       return SqlValue();
     } else if (std::holds_alternative<int64_t>(data)) {
       return SqlValue::Long(std::get<int64_t>(data));
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 700854f..7d26fed 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -731,7 +731,7 @@
                       record->AddArg("cols", base::Join(index.col_names, ", "));
                     });
 
-  Table* t = GetMutableTableOrNull(index.table_name);
+  Table* t = GetTableOrNull(index.table_name);
   if (!t) {
     return base::ErrStatus("CREATE PERFETTO INDEX: Table '%s' not found",
                            index.table_name.c_str());
@@ -759,7 +759,7 @@
                       record->AddArg("table_name", index.table_name);
                     });
 
-  Table* t = GetMutableTableOrNull(index.table_name);
+  Table* t = GetTableOrNull(index.table_name);
   if (!t) {
     return base::ErrStatus("DROP PERFETTO INDEX: Table '%s' not found",
                            index.table_name.c_str());
@@ -1114,8 +1114,7 @@
   return state ? state->runtime_table.get() : nullptr;
 }
 
-RuntimeTable* PerfettoSqlEngine::GetMutableRuntimeTableOrNull(
-    std::string_view name) {
+RuntimeTable* PerfettoSqlEngine::GetRuntimeTableOrNull(std::string_view name) {
   auto* state = runtime_table_context_->manager.FindStateByName(name);
   return state ? state->runtime_table.get() : nullptr;
 }
@@ -1126,7 +1125,7 @@
   return state ? state->static_table : nullptr;
 }
 
-Table* PerfettoSqlEngine::GetMutableStaticTableOrNull(std::string_view name) {
+Table* PerfettoSqlEngine::GetStaticTableOrNull(std::string_view name) {
   auto* state = static_table_context_->manager.FindStateByName(name);
   return state ? state->static_table : nullptr;
 }
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index f09d8c8..0828427 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -231,33 +231,12 @@
 
   // Find table (Static or Runtime) registered with engine with provided name.
   const Table* GetTableOrNull(std::string_view name) const {
-    if (auto maybe_runtime = GetRuntimeTableOrNull(name); maybe_runtime) {
-      return maybe_runtime;
+    if (const auto* r = GetRuntimeTableOrNull(name); r) {
+      return r;
     }
     return GetStaticTableOrNull(name);
   }
 
-  // Find RuntimeTable registered with engine with provided name.
-  const RuntimeTable* GetRuntimeTableOrNull(std::string_view) const;
-
-  // Find static table registered with engine with provided name.
-  const Table* GetStaticTableOrNull(std::string_view) const;
-
-  // Find table (Static or Runtime) registered with engine with provided name.
-  Table* GetMutableTableOrNull(std::string_view name) {
-    if (auto maybe_runtime = GetMutableRuntimeTableOrNull(name);
-        maybe_runtime) {
-      return maybe_runtime;
-    }
-    return GetMutableStaticTableOrNull(name);
-  }
-
-  // Find RuntimeTable registered with engine with provided name.
-  RuntimeTable* GetMutableRuntimeTableOrNull(std::string_view);
-
-  // Find static table registered with engine with provided name.
-  Table* GetMutableStaticTableOrNull(std::string_view);
-
  private:
   base::Status ExecuteCreateFunction(const PerfettoSqlParser::CreateFunction&);
 
@@ -329,6 +308,26 @@
                                  const std::string& key,
                                  const PerfettoSqlParser&);
 
+  // Find table (Static or Runtime) registered with engine with provided name.
+  Table* GetTableOrNull(std::string_view name) {
+    if (auto* maybe_runtime = GetRuntimeTableOrNull(name); maybe_runtime) {
+      return maybe_runtime;
+    }
+    return GetStaticTableOrNull(name);
+  }
+
+  // Find RuntimeTable registered with engine with provided name.
+  RuntimeTable* GetRuntimeTableOrNull(std::string_view);
+
+  // Find static table registered with engine with provided name.
+  Table* GetStaticTableOrNull(std::string_view);
+
+  // Find RuntimeTable registered with engine with provided name.
+  const RuntimeTable* GetRuntimeTableOrNull(std::string_view) const;
+
+  // Find static table registered with engine with provided name.
+  const Table* GetStaticTableOrNull(std::string_view) const;
+
   StringPool* pool_ = nullptr;
   // If true, engine will perform additional consistency checks when e.g.
   // creating tables and views.
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
index a1e8f54..b698326 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
@@ -317,29 +317,6 @@
   ASSERT_FALSE(engine_.FindPackage("bar")->modules["bar.bar"].included);
 }
 
-TEST_F(PerfettoSqlEngineTest, MismatchedRange) {
-  tables::SliceTable parent(&pool_);
-  tables::ExpectedFrameTimelineSliceTable child(&pool_, &parent);
-
-  engine_.RegisterStaticTable(&parent, "parent",
-                              tables::SliceTable::ComputeStaticSchema());
-  engine_.RegisterStaticTable(
-      &child, "child",
-      tables::ExpectedFrameTimelineSliceTable::ComputeStaticSchema());
-
-  for (uint32_t i = 0; i < 5; i++) {
-    child.Insert({});
-  }
-
-  for (uint32_t i = 0; i < 10; i++) {
-    parent.Insert({});
-  }
-
-  auto res = engine_.Execute(
-      SqlSource::FromExecuteQuery("SELECT * FROM child WHERE ts > 3"));
-  ASSERT_TRUE(res.ok()) << res.status().c_message();
-}
-
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
index 89f7e58..7ddded9 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/pprof_functions.cc
@@ -84,7 +84,7 @@
     if (!builder_.AddSample(stack, sample_values_)) {
       return base::ErrStatus("Failed to add callstack");
     }
-    return util::OkStatus();
+    return base::OkStatus();
   }
 
   void Final(sqlite3_context* ctx) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
index 1babe3d..f3a8cfd 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/stack_functions.cc
@@ -44,7 +44,7 @@
 
 using protos::pbzero::Stack;
 
-util::Status SetBytesOutputValue(const std::vector<uint8_t>& src,
+base::Status SetBytesOutputValue(const std::vector<uint8_t>& src,
                                  SqlValue& out,
                                  SqlFunction::Destructors& destructors) {
   void* dest = malloc(src.size());
@@ -54,7 +54,7 @@
   memcpy(dest, src.data(), src.size());
   out = SqlValue::Bytes(dest, src.size());
   destructors.bytes_destructor = free;
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 // CAT_STACKS(root BLOB/STRING, level_1 BLOB/STRING, …, leaf BLOB/STRING)
@@ -223,7 +223,7 @@
     }
 
     if (value->is_null()) {
-      return util::OkStatus();
+      return base::OkStatus();
     }
 
     if (value->AsLong() > std::numeric_limits<uint32_t>::max() ||
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 7898fb9..2adb7bc 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.cc
@@ -615,7 +615,7 @@
 
 SystraceSerializer::ScopedCString SystraceSerializer::SerializeToString(
     uint32_t raw_row) {
-  const auto& raw = storage_->raw_table();
+  const auto& raw = storage_->ftrace_event_table();
 
   char line[4096];
   base::StringWriter writer(line, sizeof(line));
@@ -648,7 +648,7 @@
 
 void SystraceSerializer::SerializePrefix(uint32_t raw_row,
                                          base::StringWriter* writer) {
-  const auto& raw = storage_->raw_table();
+  const auto& raw = storage_->ftrace_event_table();
   const auto& cpu_table = storage_->cpu_table();
 
   int64_t ts = raw.ts()[raw_row];
diff --git a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
index e3d1263..ebaef3f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/functions/utils.h
@@ -264,7 +264,7 @@
 
   out = SqlValue::Long(int_len);
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 struct ExtractArg : public SqlFunction {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
index 0ca4f37..cb5757e 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
@@ -55,19 +55,26 @@
       ":etm",
       ":etm_impl",
     ]
-    sources = [ "etm_decode_trace_vtable.h" ]
+    sources = [
+      "etm_decode_trace_vtable.h",
+      "etm_iterate_range_vtable.h",
+    ]
     deps = [
       "../../../../../gn:default_deps",
       "../../../../../gn:sqlite",
       "../../../../../include/perfetto/base",
       "../../../../../include/perfetto/ext/base:base",
       "../../../sqlite",
+      "../../../storage",
     ]
   }
 
   source_set("etm_impl") {
     visibility = [ ":etm" ]
-    sources = [ "etm_decode_trace_vtable.cc" ]
+    sources = [
+      "etm_decode_trace_vtable.cc",
+      "etm_iterate_range_vtable.cc",
+    ]
     deps = [
       ":etm_hdr",
       "../../../../../gn:default_deps",
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
index 83321ce..b96ddee 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
@@ -102,8 +102,7 @@
   }
   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));
+    auto value = sqlite3_column_double(res->stmt.sqlite_stmt(), 1);
     state->timestamps.push_back(ts);
     state->forest.Push(Counter{value, value});
   } while (res->stmt.Step());
@@ -239,10 +238,10 @@
   const auto& res = c->counters[c->index];
   switch (N) {
     case ColumnIndex::kMinValue:
-      sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.min));
+      sqlite::result::Double(ctx, res.min_max_counter.min);
       return SQLITE_OK;
     case ColumnIndex::kMaxValue:
-      sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.max));
+      sqlite::result::Double(ctx, res.min_max_counter.max);
       return SQLITE_OK;
     case ColumnIndex::kLastTs:
       sqlite::result::Long(ctx, res.last_ts);
@@ -250,7 +249,7 @@
     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));
+      sqlite::result::Double(ctx, res.last_counter.min);
       return SQLITE_OK;
     default:
       return sqlite::utils::SetError(t, "Bad column");
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
index 7c1337b..fa5723b 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
@@ -53,8 +53,8 @@
 // [1] https://en.wikipedia.org/wiki/Mipmap
 struct CounterMipmapOperator : sqlite::Module<CounterMipmapOperator> {
   struct Counter {
-    float min;
-    float max;
+    double min;
+    double max;
   };
   struct Agg {
     Counter operator()(const Counter& a, const Counter& b) {
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
index 9decb3c..59815e4 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.cc
@@ -29,6 +29,7 @@
 #include "src/trace_processor/importers/etm/element_cursor.h"
 #include "src/trace_processor/importers/etm/mapping_version.h"
 #include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
 #include "src/trace_processor/importers/etm/util.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
@@ -82,7 +83,8 @@
       isa TEXT,
       start_address INTEGER,
       end_address INTEGER,
-      mapping_id INTEGER
+      mapping_id INTEGER,
+      instruction_range BLOB HIDDEN
     )
   )";
 
@@ -98,7 +100,8 @@
   kIsa,
   kStartAddress,
   kEndAddress,
-  kMappingId
+  kMappingId,
+  kInstructionRange
 };
 
 constexpr char kTraceIdEqArg = 't';
@@ -237,6 +240,12 @@
         sqlite::result::Long(ctx, cursor_.mapping()->id().value);
       }
       break;
+    case ColumnIndex::kInstructionRange:
+      if (cursor_.has_instruction_range()) {
+        sqlite::result::UniquePointer(ctx, cursor_.GetInstructionRange(),
+                                      InstructionRangeSqlValue::kPtrType);
+      }
+      break;
   }
 
   return SQLITE_OK;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc
new file mode 100644
index 0000000..a97069c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.cc
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2021 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/etm_iterate_range_vtable.h"
+#include <opencsd/ocsd_if_types.h>
+
+#include <cstring>
+#include <memory>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/importers/etm/opencsd.h"
+#include "src/trace_processor/importers/etm/sql_values.h"
+#include "src/trace_processor/importers/etm/storage_handle.h"
+#include "src/trace_processor/importers/etm/types.h"
+#include "src/trace_processor/importers/etm/util.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_value.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor::etm {
+namespace {
+
+static constexpr char kSchema[] = R"(
+    CREATE TABLE x(
+      instruction_index INTEGER,
+      address INTEGER,
+      opcode INTEGER,
+      type TEXT,
+      branch_address INTEGER,
+      is_conditional INTEGER,
+      is_link INTEGER,
+      sub_type TEXT,
+      instruction_range BLOB HIDDEN
+    )
+  )";
+
+enum class ColumnIndex {
+  kInstructionIndex,
+  kAddress,
+  kOpcode,
+  kType,
+  kBranchAddress,
+  kIsConditional,
+  kIsLink,
+  kSubType,
+  kInstructionRange
+};
+
+constexpr char kInstructionRangeEqArg = 'r';
+
+class IntructionCursor : public sqlite::Module<EtmIterateRangeVtable>::Cursor {
+ public:
+  explicit IntructionCursor(TraceStorage* storage) : storage_(storage) {}
+  int Filter(int, const char* idxStr, int argc, sqlite3_value** argv) {
+    std::optional<const InstructionRangeSqlValue*> range;
+    if (argc != static_cast<int>(strlen(idxStr))) {
+      return sqlite::utils::SetError(pVtab, "Invalid idxStr");
+    }
+    for (; *idxStr != 0; ++idxStr, ++argv) {
+      switch (*idxStr) {
+        case kInstructionRangeEqArg: {
+          range = sqlite::value::Pointer<InstructionRangeSqlValue>(
+              *argv, InstructionRangeSqlValue::kPtrType);
+          break;
+        }
+        default:
+          return sqlite::utils::SetError(pVtab, "Invalid idxStr");
+      }
+    }
+
+    if (!range.has_value()) {
+      return sqlite::utils::SetError(pVtab, "Invalid idxStr, no range");
+    }
+
+    Reset(*range);
+    return SQLITE_OK;
+  }
+
+  void Next() {
+    ++instruction_index_;
+    ptr_ += instr_info_.instr_size;
+    if (ptr_ == end_) {
+      return;
+    }
+
+    instr_info_.instr_addr += instr_info_.instr_size;
+    instr_info_.isa = instr_info_.next_isa;
+    FeedDecoder();
+  }
+
+  bool Eof() { return ptr_ == end_; }
+
+  int Column(sqlite3_context* ctx, int raw_n) {
+    switch (static_cast<ColumnIndex>(raw_n)) {
+      case ColumnIndex::kInstructionIndex:
+        sqlite::result::Long(ctx, instruction_index_);
+        break;
+      case ColumnIndex::kAddress:
+        sqlite::result::Long(ctx, static_cast<int64_t>(instr_info_.instr_addr));
+        break;
+      case ColumnIndex::kOpcode:
+        sqlite::result::Long(ctx, instr_info_.opcode);
+        break;
+      case ColumnIndex::kType:
+        sqlite::result::StaticString(ctx, ToString(instr_info_.type));
+        break;
+      case ColumnIndex::kBranchAddress:
+        if (instr_info_.type == OCSD_INSTR_BR ||
+            instr_info_.type == OCSD_INSTR_BR_INDIRECT) {
+          sqlite::result::Long(ctx,
+                               static_cast<int64_t>(instr_info_.branch_addr));
+        }
+        break;
+      case ColumnIndex::kIsConditional:
+        sqlite::result::Long(ctx, instr_info_.is_conditional);
+        break;
+      case ColumnIndex::kIsLink:
+        sqlite::result::Long(ctx, instr_info_.is_link);
+        break;
+      case ColumnIndex::kSubType:
+        sqlite::result::StaticString(ctx, ToString(instr_info_.sub_type));
+        break;
+      case ColumnIndex::kInstructionRange:
+        break;
+    }
+
+    return SQLITE_OK;
+  }
+
+ private:
+  void FeedDecoder() {
+    PERFETTO_CHECK(static_cast<size_t>(end_ - ptr_) >=
+                   sizeof(instr_info_.opcode));
+    memcpy(&instr_info_.opcode, ptr_, sizeof(instr_info_.opcode));
+    inst_decoder_.DecodeInstruction(&instr_info_);
+  }
+
+  void Reset(const InstructionRangeSqlValue* range) {
+    if (!range) {
+      ptr_ = nullptr;
+      end_ = nullptr;
+      return;
+    }
+    const auto& config =
+        StorageHandle(storage_).GetEtmV4Config(range->config_id);
+    instr_info_.pe_type.arch = config.etm_v4_config().archVersion();
+    instr_info_.pe_type.profile = config.etm_v4_config().coreProfile();
+    instr_info_.dsb_dmb_waypoints = 0;  // Not used in ETM
+    instr_info_.wfi_wfe_branch = config.etm_v4_config().wfiwfeBranch();
+    instr_info_.isa = range->isa;
+    instr_info_.instr_addr = range->st_addr;
+
+    ptr_ = range->start;
+    end_ = range->end;
+    instruction_index_ = 0;
+    FeedDecoder();
+  }
+
+  TraceStorage* storage_;
+  const uint8_t* ptr_ = nullptr;
+  const uint8_t* end_ = nullptr;
+  ocsd_instr_info instr_info_;
+  TrcIDecode inst_decoder_;
+  uint32_t instruction_index_ = 0;
+};
+
+IntructionCursor* GetInstructionCursor(sqlite3_vtab_cursor* cursor) {
+  return static_cast<IntructionCursor*>(cursor);
+}
+
+}  // namespace
+
+int EtmIterateRangeVtable::Connect(sqlite3* db,
+                                   void* ctx,
+                                   int,
+                                   const char* const*,
+                                   sqlite3_vtab** vtab,
+                                   char**) {
+  if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+    return ret;
+  }
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->storage = GetContext(ctx);
+  *vtab = res.release();
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Disconnect(sqlite3_vtab* vtab) {
+  delete GetVtab(vtab);
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::BestIndex(sqlite3_vtab* tab,
+                                     sqlite3_index_info* info) {
+  bool seen_range = false;
+  int argv_index = 1;
+  std::string idx_str;
+  for (int i = 0; i < info->nConstraint; ++i) {
+    auto& in = info->aConstraint[i];
+    auto& out = info->aConstraintUsage[i];
+
+    if (in.iColumn == static_cast<int>(ColumnIndex::kInstructionRange)) {
+      if (!in.usable) {
+        return SQLITE_CONSTRAINT;
+      }
+      if (in.op != SQLITE_INDEX_CONSTRAINT_EQ) {
+        return sqlite::utils::SetError(
+            tab, "instruction_range only supports equality constraints");
+      }
+      idx_str += kInstructionRangeEqArg;
+      out.argvIndex = argv_index++;
+      out.omit = true;
+      seen_range = true;
+      continue;
+    }
+  }
+
+  if (!seen_range) {
+    return sqlite::utils::SetError(tab,
+                                   "Constraint required on instruction_range");
+  }
+
+  info->idxStr = sqlite3_mprintf("%s", idx_str.c_str());
+  info->needToFreeIdxStr = true;
+
+  if (info->nOrderBy == 1 &&
+      info->aOrderBy[0].iColumn ==
+          static_cast<int>(ColumnIndex::kInstructionIndex) &&
+      !info->aOrderBy[0].desc) {
+    info->orderByConsumed = true;
+  }
+
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Open(sqlite3_vtab* vtab,
+                                sqlite3_vtab_cursor** cursor) {
+  *cursor = new IntructionCursor(GetVtab(vtab)->storage);
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Close(sqlite3_vtab_cursor* cursor) {
+  delete GetInstructionCursor(cursor);
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Filter(sqlite3_vtab_cursor* cur,
+                                  int idxNum,
+                                  const char* idxStr,
+                                  int argc,
+                                  sqlite3_value** argv) {
+  GetInstructionCursor(cur)->Filter(idxNum, idxStr, argc, argv);
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Next(sqlite3_vtab_cursor* cur) {
+  GetInstructionCursor(cur)->Next();
+  return SQLITE_OK;
+}
+
+int EtmIterateRangeVtable::Eof(sqlite3_vtab_cursor* cur) {
+  return GetInstructionCursor(cur)->Eof();
+}
+
+int EtmIterateRangeVtable::Column(sqlite3_vtab_cursor* cur,
+                                  sqlite3_context* ctx,
+                                  int raw_n) {
+  return GetInstructionCursor(cur)->Column(ctx, raw_n);
+}
+
+int EtmIterateRangeVtable::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor::etm
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h
new file mode 100644
index 0000000..b9b7aab
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h
@@ -0,0 +1,69 @@
+/*
+ * 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_ETM_ITERATE_RANGE_VTABLE_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_ETM_ITERATE_RANGE_VTABLE_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor::etm {
+
+struct InstructionRangeSqlValue;
+
+struct EtmIterateRangeVtable : sqlite::Module<EtmIterateRangeVtable> {
+  using Context = TraceStorage;
+  struct Vtab : sqlite::Module<EtmIterateRangeVtable>::Vtab {
+    TraceStorage* storage;
+  };
+
+  static constexpr auto kType = kEponymousOnly;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
+
+  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*);
+
+  // This needs to happen at the end as it depends on the functions
+  // defined above.
+  static constexpr sqlite3_module kModule = CreateModule();
+};
+
+}  // namespace perfetto::trace_processor::etm
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_ETM_ITERATE_RANGE_VTABLE_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
index a73c7d9..3853faa 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/BUILD.gn
@@ -29,22 +29,18 @@
     "dfs_weight_bounded.h",
     "experimental_annotated_stack.cc",
     "experimental_annotated_stack.h",
-    "experimental_counter_dur.cc",
-    "experimental_counter_dur.h",
     "experimental_flamegraph.cc",
     "experimental_flamegraph.h",
     "experimental_flat_slice.cc",
     "experimental_flat_slice.h",
-    "experimental_sched_upid.cc",
-    "experimental_sched_upid.h",
     "experimental_slice_layout.cc",
     "experimental_slice_layout.h",
     "flamegraph_construction_algorithms.cc",
     "flamegraph_construction_algorithms.h",
-    "winscope_proto_to_args_with_defaults.cc",
-    "winscope_proto_to_args_with_defaults.h",
     "table_info.cc",
     "table_info.h",
+    "winscope_proto_to_args_with_defaults.cc",
+    "winscope_proto_to_args_with_defaults.h",
   ]
   deps = [
     ":tables",
@@ -96,7 +92,6 @@
     "ancestor_unittest.cc",
     "connected_flow_unittest.cc",
     "descendant_unittest.cc",
-    "experimental_counter_dur_unittest.cc",
     "experimental_flat_slice_unittest.cc",
     "experimental_slice_layout_unittest.cc",
   ]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc
deleted file mode 100644
index 48407cc..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.cc
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2020 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/table_functions/experimental_counter_dur.h"
-
-#include <cstdint>
-#include <memory>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-
-namespace perfetto::trace_processor {
-namespace tables {
-
-ExperimentalCounterDurTable::~ExperimentalCounterDurTable() = default;
-
-}  // namespace tables
-
-ExperimentalCounterDur::ExperimentalCounterDur(
-    const tables::CounterTable& table)
-    : counter_table_(&table) {}
-ExperimentalCounterDur::~ExperimentalCounterDur() = default;
-
-Table::Schema ExperimentalCounterDur::CreateSchema() {
-  return tables::ExperimentalCounterDurTable::ComputeStaticSchema();
-}
-
-std::string ExperimentalCounterDur::TableName() {
-  return tables::ExperimentalCounterDurTable::Name();
-}
-
-uint32_t ExperimentalCounterDur::EstimateRowCount() {
-  return counter_table_->row_count();
-}
-
-base::StatusOr<std::unique_ptr<Table>> ExperimentalCounterDur::ComputeTable(
-    const std::vector<SqlValue>& arguments) {
-  PERFETTO_CHECK(arguments.empty());
-  if (!counter_dur_table_) {
-    counter_dur_table_ = tables::ExperimentalCounterDurTable::ExtendParent(
-        *counter_table_, ComputeDurColumn(*counter_table_),
-        ComputeDeltaColumn(*counter_table_));
-  }
-  return std::make_unique<Table>(counter_dur_table_->Copy());
-}
-
-// static
-ColumnStorage<int64_t> ExperimentalCounterDur::ComputeDurColumn(
-    const CounterTable& table) {
-  // Keep track of the last seen row for each track id.
-  std::unordered_map<TrackId, CounterTable::RowNumber> last_row_for_track_id;
-  ColumnStorage<int64_t> dur;
-
-  for (auto table_it = table.IterateRows(); table_it; ++table_it) {
-    // Check if we already have a previous row for the current track id.
-    TrackId track_id = table_it.track_id();
-    auto it = last_row_for_track_id.find(track_id);
-    if (it == last_row_for_track_id.end()) {
-      // This means we don't have any row - start tracking this row for the
-      // future.
-      last_row_for_track_id.emplace(track_id, table_it.row_number());
-    } else {
-      // This means we have an previous row for the current track id. Update
-      // the duration of the previous row to be up to the current ts.
-      CounterTable::RowNumber old_row = it->second;
-      it->second = table_it.row_number();
-      dur.Set(old_row.row_number(),
-              table_it.ts() - old_row.ToRowReference(table).ts());
-    }
-    // Append -1 to mark this event as not having been finished. On a later
-    // row, we may set this to have the correct value.
-    dur.Append(-1);
-  }
-  return dur;
-}
-
-// static
-ColumnStorage<double> ExperimentalCounterDur::ComputeDeltaColumn(
-    const CounterTable& table) {
-  // Keep track of the last seen row for each track id.
-  std::unordered_map<TrackId, CounterTable::RowNumber> last_row_for_track_id;
-  ColumnStorage<double> delta;
-
-  for (auto table_it = table.IterateRows(); table_it; ++table_it) {
-    // Check if we already have a previous row for the current track id.
-    TrackId track_id = table_it.track_id();
-    auto it = last_row_for_track_id.find(track_id);
-    if (it == last_row_for_track_id.end()) {
-      // This means we don't have any row - start tracking this row for the
-      // future.
-      last_row_for_track_id.emplace(track_id, table_it.row_number());
-    } else {
-      // This means we have an previous row for the current track id. Update
-      // the duration of the previous row to be up to the current ts.
-      CounterTable::RowNumber old_row = it->second;
-      it->second = table_it.row_number();
-      delta.Set(old_row.row_number(),
-                table_it.value() - old_row.ToRowReference(table).value());
-    }
-    delta.Append(0);
-  }
-  return delta;
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h
deleted file mode 100644
index 3e1999b..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2020 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_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
-
-#include <cstdint>
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-
-namespace perfetto::trace_processor {
-
-class ExperimentalCounterDur : public StaticTableFunction {
- public:
-  using CounterTable = tables::CounterTable;
-
-  explicit ExperimentalCounterDur(const CounterTable& table);
-  virtual ~ExperimentalCounterDur() override;
-
-  Table::Schema CreateSchema() override;
-  std::string TableName() override;
-  uint32_t EstimateRowCount() override;
-  base::StatusOr<std::unique_ptr<Table>> ComputeTable(
-      const std::vector<SqlValue>& arguments) override;
-
-  // public + static for testing
-  static ColumnStorage<int64_t> ComputeDurColumn(const CounterTable& table);
-  static ColumnStorage<double> ComputeDeltaColumn(const CounterTable& table);
-
- private:
-  const CounterTable* counter_table_ = nullptr;
-  std::unique_ptr<Table> counter_dur_table_;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_COUNTER_DUR_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc
deleted file mode 100644
index 783c2a0..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur_unittest.cc
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 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/table_functions/experimental_counter_dur.h"
-
-#include <cstdint>
-
-#include "src/trace_processor/containers/string_pool.h"
-#include "src/trace_processor/tables/counter_tables_py.h"
-#include "src/trace_processor/tables/track_tables_py.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto::trace_processor {
-namespace {
-
-tables::CounterTable::Row CounterRow(int64_t ts, uint32_t track_id) {
-  tables::CounterTable::Row row;
-  row.ts = ts;
-  row.track_id = tables::TrackTable::Id{track_id};
-  return row;
-}
-
-TEST(ExperimentalCounterDur, SmokeDur) {
-  StringPool pool;
-  tables::CounterTable table(&pool);
-
-  table.Insert(CounterRow(100 /* ts */, 1 /* track_id */));
-  table.Insert(CounterRow(102 /* ts */, 2 /* track_id */));
-  table.Insert(CounterRow(105 /* ts */, 1 /* track_id */));
-  table.Insert(CounterRow(105 /* ts */, 3 /* track_id */));
-  table.Insert(CounterRow(105 /* ts */, 2 /* track_id */));
-  table.Insert(CounterRow(110 /* ts */, 2 /* track_id */));
-
-  auto dur = ExperimentalCounterDur::ComputeDurColumn(table);
-  ASSERT_EQ(dur.size(), table.row_count());
-
-  ASSERT_EQ(dur.Get(0), 5);
-  ASSERT_EQ(dur.Get(1), 3);
-  ASSERT_EQ(dur.Get(2), -1);
-  ASSERT_EQ(dur.Get(3), -1);
-  ASSERT_EQ(dur.Get(4), 5);
-  ASSERT_EQ(dur.Get(5), -1);
-}
-
-}  // namespace
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
index 6d4ff85..ccd9bf5 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.cc
@@ -86,7 +86,7 @@
     row.track_id = track_id;
     row.category = kNullStringId;
     row.name = kNullStringId;
-    row.arg_set_id = kInvalidArgSetId;
+    row.arg_set_id = std::nullopt;
     row.source_id = std::nullopt;
     row.start_bound = start_bound;
     row.end_bound = end_bound;
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc
deleted file mode 100644
index 6c4b43c..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.cc
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2020 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/table_functions/experimental_sched_upid.h"
-
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/tables_py.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/metadata_tables_py.h"
-#include "src/trace_processor/tables/sched_tables_py.h"
-
-namespace perfetto::trace_processor {
-namespace tables {
-
-ExperimentalSchedUpidTable::~ExperimentalSchedUpidTable() = default;
-
-}  // namespace tables
-
-ExperimentalSchedUpid::ExperimentalSchedUpid(
-    const tables::SchedSliceTable& sched,
-    const tables::ThreadTable& thread)
-    : sched_slice_table_(&sched), thread_table_(&thread) {}
-ExperimentalSchedUpid::~ExperimentalSchedUpid() = default;
-
-Table::Schema ExperimentalSchedUpid::CreateSchema() {
-  return tables::ExperimentalSchedUpidTable::ComputeStaticSchema();
-}
-
-std::string ExperimentalSchedUpid::TableName() {
-  return tables::ExperimentalSchedUpidTable::Name();
-}
-
-uint32_t ExperimentalSchedUpid::EstimateRowCount() {
-  return sched_slice_table_->row_count();
-}
-
-base::StatusOr<std::unique_ptr<Table>> ExperimentalSchedUpid::ComputeTable(
-    const std::vector<SqlValue>& arguments) {
-  PERFETTO_CHECK(arguments.empty());
-  if (!sched_upid_table_) {
-    sched_upid_table_ = tables::ExperimentalSchedUpidTable::ExtendParent(
-        *sched_slice_table_, ComputeUpidColumn());
-  }
-  return std::make_unique<Table>(sched_upid_table_->Copy());
-}
-
-ColumnStorage<std::optional<UniquePid>>
-ExperimentalSchedUpid::ComputeUpidColumn() {
-  ColumnStorage<std::optional<UniquePid>> upid;
-  for (uint32_t i = 0; i < sched_slice_table_->row_count(); ++i) {
-    upid.Append(thread_table_->upid()[sched_slice_table_->utid()[i]]);
-  }
-  return upid;
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h
deleted file mode 100644
index 0eecc4f..0000000
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 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_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
-#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
-
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/trace_processor/basic_types.h"
-#include "src/trace_processor/db/column_storage.h"
-#include "src/trace_processor/db/table.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/metadata_tables_py.h"
-#include "src/trace_processor/tables/sched_tables_py.h"
-
-namespace perfetto::trace_processor {
-
-class ExperimentalSchedUpid : public StaticTableFunction {
- public:
-  ExperimentalSchedUpid(const tables::SchedSliceTable&,
-                        const tables::ThreadTable&);
-  virtual ~ExperimentalSchedUpid() override;
-
-  Table::Schema CreateSchema() override;
-  std::string TableName() override;
-  uint32_t EstimateRowCount() override;
-  base::StatusOr<std::unique_ptr<Table>> ComputeTable(
-      const std::vector<SqlValue>& arguments) override;
-
- private:
-  ColumnStorage<std::optional<UniquePid>> ComputeUpidColumn();
-
-  const tables::SchedSliceTable* sched_slice_table_;
-  const tables::ThreadTable* thread_table_;
-  std::unique_ptr<Table> sched_upid_table_;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_EXPERIMENTAL_SCHED_UPID_H_
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
index 401de7b..282d27c 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.cc
@@ -104,27 +104,15 @@
   auto table = std::make_unique<TableInfoTable>(string_pool_);
   auto table_name_id = string_pool_->InternString(table_name.c_str());
 
-  // Find static table
-  const Table* static_table = engine_->GetStaticTableOrNull(table_name);
-  if (static_table) {
-    for (auto& row : GetColInfoRows(static_table->columns(), string_pool_)) {
+  // Find table
+  const Table* t = engine_->GetTableOrNull(table_name);
+  if (t) {
+    for (auto& row : GetColInfoRows(t->columns(), string_pool_)) {
       row.table_name = table_name_id;
       table->Insert(row);
     }
     return std::unique_ptr<Table>(std::move(table));
   }
-
-  // Find runtime table
-  const RuntimeTable* runtime_table =
-      engine_->GetRuntimeTableOrNull(table_name);
-  if (runtime_table) {
-    for (auto& row : GetColInfoRows(runtime_table->columns(), string_pool_)) {
-      row.table_name = table_name_id;
-      table->Insert(row);
-    }
-    return std::unique_ptr<Table>(std::move(table));
-  }
-
   return base::ErrStatus("Perfetto table '%s' not found.", table_name.c_str());
 }
 
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
index 22999ee..0b63b7f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/tables.py
@@ -125,25 +125,6 @@
     ],
     parent=STACK_PROFILE_CALLSITE_TABLE)
 
-EXPERIMENTAL_COUNTER_DUR_TABLE = Table(
-    python_module=__file__,
-    class_name="ExperimentalCounterDurTable",
-    sql_name="experimental_counter_dur",
-    columns=[
-        C("dur", CppInt64()),
-        C("delta", CppDouble()),
-    ],
-    parent=COUNTER_TABLE)
-
-EXPERIMENTAL_SCHED_UPID_TABLE = Table(
-    python_module=__file__,
-    class_name="ExperimentalSchedUpidTable",
-    sql_name="__intrinsic_sched_upid",
-    columns=[
-        C("upid", CppOptional(CppTableId(PROCESS_TABLE))),
-    ],
-    parent=SCHED_SLICE_TABLE)
-
 EXPERIMENTAL_SLICE_LAYOUT_TABLE = Table(
     python_module=__file__,
     class_name="ExperimentalSliceLayoutTable",
@@ -189,8 +170,6 @@
     DESCENDANT_SLICE_TABLE,
     DFS_WEIGHT_BOUNDED_TABLE,
     EXPERIMENTAL_ANNOTATED_CALLSTACK_TABLE,
-    EXPERIMENTAL_COUNTER_DUR_TABLE,
-    EXPERIMENTAL_SCHED_UPID_TABLE,
     EXPERIMENTAL_SLICE_LAYOUT_TABLE,
     TABLE_INFO_TABLE,
 ]
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
index ee8e600..0ba91d4 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.cc
@@ -16,9 +16,21 @@
 
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
 
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/base64.h"
 #include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
+#include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
@@ -84,7 +96,7 @@
     Row r;
     SetColumnsAndInsertRow(key, r);
   }
-  void AddPointer(const Key&, const void*) override {
+  void AddPointer(const Key&, uint64_t) override {
     PERFETTO_FATAL("Unsupported");
   }
   bool AddJson(const Key&, const protozero::ConstChars&) override {
@@ -150,7 +162,7 @@
 
 WinscopeProtoToArgsWithDefaults::WinscopeProtoToArgsWithDefaults(
     StringPool* string_pool,
-    PerfettoSqlEngine* engine,
+    const PerfettoSqlEngine* engine,
     TraceProcessorContext* context)
     : string_pool_(string_pool), engine_(engine), context_(context) {}
 
@@ -165,7 +177,7 @@
   }
   std::string table_name = arguments[0].AsString();
 
-  const Table* static_table = engine_->GetStaticTableOrNull(table_name);
+  const Table* static_table = engine_->GetTableOrNull(table_name);
   if (!static_table) {
     return base::ErrStatus("Failed to find %s table.", table_name.c_str());
   }
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
index 91ab8c8..84b3a47 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h
@@ -17,7 +17,13 @@
 #ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_PROTO_TO_ARGS_WITH_DEFAULTS_H_
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_TABLE_FUNCTIONS_WINSCOPE_PROTO_TO_ARGS_WITH_DEFAULTS_H_
 
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
 #include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
@@ -30,7 +36,7 @@
 class WinscopeProtoToArgsWithDefaults : public StaticTableFunction {
  public:
   explicit WinscopeProtoToArgsWithDefaults(StringPool*,
-                                           PerfettoSqlEngine*,
+                                           const PerfettoSqlEngine*,
                                            TraceProcessorContext* context);
 
   Table::Schema CreateSchema() override;
@@ -41,7 +47,7 @@
 
  private:
   StringPool* string_pool_ = nullptr;
-  PerfettoSqlEngine* engine_ = nullptr;
+  const PerfettoSqlEngine* engine_ = nullptr;
   TraceProcessorContext* context_ = nullptr;
 };
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index 92150ae..09386af 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -20,6 +20,7 @@
 perfetto_amalgamated_sql_header("stdlib") {
   deps = [
     "android",
+    "appleos",
     "callstacks",
     "chrome:chrome_sql",
     "counters",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
index 6e5956b..b8d70c4 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
@@ -66,7 +66,8 @@
   s.id,
   s.process_name,
   thread.utid,
-  s.upid
+  s.upid,
+  s.ts + s.dur AS ts_end
 FROM thread_slice s JOIN
 thread USING (utid)
 WHERE
@@ -81,6 +82,7 @@
   tx.binder_txn_id AS id,
   tx.client_process as process_name,
   tx.client_utid as utid,
-  tx.client_upid as upid
+  tx.client_upid as upid,
+  tx.client_ts + tx.client_dur AS ts_end
 FROM android_binder_txns AS tx
 WHERE aidl_name IS NOT NULL AND is_sync = 1;
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 ff0c822..022260f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/frames/timeline.sql
@@ -24,7 +24,7 @@
     glob_str STRING
 ) RETURNS TABLE (
     -- Frame slice.
-    id JOINID(slice.id),
+    id ID(slice.id),
     -- Parsed frame id.
     frame_id LONG,
     -- Utid.
@@ -55,7 +55,7 @@
 CREATE PERFETTO TABLE android_frames_choreographer_do_frame(
     -- Choreographer#doFrame slice. Slice with the name "Choreographer#doFrame
     -- {frame id}".
-    id JOINID(slice.id),
+    id ID(slice.id),
     -- Frame id. Taken as the value behind "Choreographer#doFrame" in slice
     -- name.
     frame_id LONG,
@@ -82,7 +82,7 @@
 -- notifications).
 CREATE PERFETTO TABLE android_frames_draw_frame(
     -- DrawFrame slice. Slice with the name "DrawFrame {frame id}".
-    id JOINID(slice.id),
+    id ID(slice.id),
     -- Frame id. Taken as the value behind "DrawFrame" in slice
     -- name.
     frame_id LONG,
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/io.sql b/src/trace_processor/perfetto_sql/stdlib/android/io.sql
index e92dbcc..b122b91 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/io.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/io.sql
@@ -77,7 +77,7 @@
       EXTRACT_ARG(arg_set_id, 'dev') AS dev,
       EXTRACT_ARG(arg_set_id, 'ino') AS ino,
       EXTRACT_ARG(arg_set_id, 'copied') AS copied
-    FROM raw
+    FROM ftrace_event
     WHERE name GLOB 'f2fs_write_end*'
   )
 SELECT
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
index 39c0786..6c06c87 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
@@ -15,7 +15,7 @@
 -- Screenshot slices, used in perfetto UI.
 CREATE PERFETTO TABLE android_screenshots(
   -- Id of the screenshot slice.
-  id JOINID(slice.id),
+  id ID(slice.id),
   -- Slice timestamp.
   ts TIMESTAMP,
   -- Slice duration, should be typically 0 since screeenshot slices are of instant
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn
new file mode 100644
index 0000000..0ddc0a0
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/BUILD.gn
@@ -0,0 +1,20 @@
+# Copyright (C) 2025 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("appleos") {
+  sources = []
+  deps = [ "instruments" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn
new file mode 100644
index 0000000..b964f2e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright (C) 2025 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("instruments") {
+  sources = [ "samples.sql" ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql
new file mode 100644
index 0000000..c0c1c40
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/appleos/instruments/samples.sql
@@ -0,0 +1,62 @@
+--
+-- Copyright 2025 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 callstacks.stack_profile;
+
+CREATE PERFETTO TABLE _appleos_instruments_raw_callstacks AS
+SELECT *
+FROM _callstacks_for_callsites!((
+  SELECT p.callsite_id
+  FROM instruments_sample p
+)) c
+ORDER BY c.id;
+
+-- Table summarising the callstacks captured during all
+-- instruments samples in the trace.
+--
+-- Specifically, this table returns a tree containing all
+-- the callstacks seen during the trace with `self_count`
+-- equal to the number of samples with that frame as the
+-- leaf and `cumulative_count` equal to the number of
+-- samples with the frame anywhere in the tree.
+CREATE PERFETTO TABLE appleos_instruments_samples_summary_tree(
+  -- The id of the callstack. A callstack in this context
+  -- is a unique set of frames up to the root.
+  id LONG,
+  -- The id of the parent callstack for this callstack.
+  parent_id LONG,
+  -- The function name of the frame for this callstack.
+  name STRING,
+  -- The name of the mapping containing the frame. This
+  -- can be a native binary, library, or JIT.
+  mapping_name STRING,
+  -- The name of the file containing the function.
+  source_file STRING,
+  -- The line number in the file the function is located at.
+  line_number LONG,
+  -- The number of samples with this function as the leaf
+  -- frame.
+  self_count LONG,
+  -- The number of samples with this function appearing
+  -- anywhere on the callstack.
+  cumulative_count LONG
+) AS
+SELECT r.*, a.cumulative_count
+FROM _callstacks_self_to_cumulative!((
+  SELECT id, parent_id, self_count
+  FROM _appleos_instruments_raw_callstacks
+)) a
+JOIN _appleos_instruments_raw_callstacks r USING (id)
+ORDER BY r.id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
index d8bc406..2785e51 100644
--- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -15,6 +15,7 @@
 
 INCLUDE PERFETTO MODULE graphs.hierarchy;
 INCLUDE PERFETTO MODULE graphs.scan;
+INCLUDE PERFETTO MODULE v8.jit;
 
 CREATE PERFETTO TABLE _callstack_spf_summary AS
 SELECT
@@ -52,9 +53,11 @@
     s.id - 1
   ) AS parent_symbol_id,
   f.id AS frame_id,
+  jf.jit_code_id AS jit_code_id,
   s.id IS f.max_symbol_id AS is_leaf
 FROM stack_profile_callsite c
 JOIN _callstack_spf_summary f ON c.frame_id = f.id
+LEFT JOIN __intrinsic_jit_frame jf ON jf.frame_id = f.id
 LEFT JOIN stack_profile_symbol s USING (symbol_set_id)
 LEFT JOIN stack_profile_callsite p ON c.parent_id = p.id
 LEFT JOIN _callstack_spf_summary pf ON p.frame_id = pf.id
@@ -67,17 +70,29 @@
   -- TODO(lalitm): consider demangling in a separate table as
   -- demangling is suprisingly inefficient and is taking a
   -- significant fraction of the runtime on big traces.
-  IFNULL(
+  COALESCE(
+    'JS: ' || IIF(jsf.name = "", "(anonymous)", jsf.name) || ':' || jsf.line || ':' || jsf.col || ' [' || LOWER(jsc.tier) || ']',
+    'WASM: ' || wc.function_name || ' [' || LOWER(wc.tier) || ']',
+    'REGEXP: ' || rc.pattern,
+    'V8: ' || v8c.function_name,
+    'JIT: ' || jc.function_name,
     DEMANGLE(COALESCE(s.name, f.deobfuscated_name, f.name)),
     COALESCE(s.name, f.deobfuscated_name, f.name, '[Unknown]')
   ) AS name,
   f.mapping AS mapping_id,
   s.source_file,
-  s.line_number,
+  COALESCE(jsf.line, s.line_number) as line_number,
+  COALESCE(jsf.col, 0) as column_number,
   c.callsite_id,
   c.is_leaf AS is_leaf_function_in_callsite_frame
 FROM _callstack_spc_raw_forest c
 JOIN stack_profile_frame f ON c.frame_id = f.id
+LEFT JOIN _v8_js_code jsc USING(jit_code_id)
+LEFT JOIN v8_js_function jsf USING(v8_js_function_id)
+LEFT JOIN _v8_internal_code v8c USING(jit_code_id)
+LEFT JOIN _v8_wasm_code wc USING(jit_code_id)
+LEFT JOIN _v8_regexp_code rc USING(jit_code_id)
+LEFT JOIN __intrinsic_jit_code jc ON c.jit_code_id = jc.id
 LEFT JOIN stack_profile_symbol s ON c.symbol_id = s.id
 LEFT JOIN _callstack_spc_raw_forest p ON
   p.callsite_id = c.parent_callsite_id
@@ -172,4 +187,4 @@
       JOIN $callstacks r USING (id)
     )
   ) a
-)
\ No newline at end of file
+)
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 d526f64..97d64fa 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql
@@ -155,7 +155,7 @@
   -- Whether this is the first input that was presented in frame
   -- `presented_in_frame_id`.
   is_first_scroll_update_in_frame BOOL,
-  -- Whether the corresponding input event was coalesced into another.
+  -- Input generation timestamp (from the Android system).
   generation_ts TIMESTAMP,
   -- Duration from input generation to when the browser received the input.
   generation_to_browser_main_dur DURATION,
@@ -252,7 +252,13 @@
   -- No applicable utid (duration between two threads).
   -- No applicable slice id (duration between two threads).
   generation_ts,
-  touch_move_received_ts - generation_ts AS generation_to_browser_main_dur,
+  -- Flings don't have a touch move event so make GenerationToBrowserMain span
+  -- all the way to the creation of the gesture scroll update.
+  IIF(
+    is_inertial AND touch_move_received_ts IS NULL,
+    scroll_update_created_ts,
+    touch_move_received_ts
+  ) - generation_ts AS generation_to_browser_main_dur,
   -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   browser_utid,
   touch_move_received_slice_id,
@@ -355,7 +361,6 @@
   chrome_event_latency.buffer_available_timestamp,
   chrome_event_latency.buffer_ready_timestamp,
   chrome_event_latency.latch_timestamp,
-  chrome_event_latency.swap_end_timestamp,
   chrome_event_latency.presentation_timestamp
 FROM _chrome_scroll_update_refs refs
 LEFT JOIN chrome_event_latencies chrome_event_latency
@@ -484,12 +489,10 @@
   viz_swap_buffers_to_latch_dur DURATION,
   -- Timestamp for `EventLatency`'s `LatchToSwapEnd` step.
   latch_timestamp TIMESTAMP,
-  -- Duration of `EventLatency`'s `LatchToSwapEnd` step.
-  viz_latch_to_swap_end_dur DURATION,
-  -- Timestamp for `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
-  swap_end_timestamp TIMESTAMP,
-  -- Duration of `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
-  swap_end_to_presentation_dur DURATION,
+  -- Duration of either `EventLatency`'s `LatchToSwapEnd` +
+  -- `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation`
+  -- step.
+  viz_latch_to_presentation_dur DURATION,
   -- Presentation timestamp for the frame.
   presentation_timestamp TIMESTAMP
 ) AS
@@ -550,7 +553,6 @@
     -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
     -- Timestamps
     latch_timestamp,
-    swap_end_timestamp,
     presentation_timestamp
   FROM _scroll_update_frame_timestamps_and_metadata
 )
@@ -615,10 +617,7 @@
   latch_timestamp - viz_swap_buffers_end_ts AS viz_swap_buffers_to_latch_dur,
   -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   latch_timestamp,
-  swap_end_timestamp - latch_timestamp AS viz_latch_to_swap_end_dur,
-  -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-  swap_end_timestamp,
-  presentation_timestamp - swap_end_timestamp AS swap_end_to_presentation_dur,
+  presentation_timestamp - latch_timestamp AS viz_latch_to_presentation_dur,
   -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   presentation_timestamp
 FROM processed_timestamps_and_metadata;
@@ -841,12 +840,10 @@
   viz_swap_buffers_to_latch_dur DURATION,
   -- Timestamp for `EventLatency`'s `LatchToSwapEnd` step.
   latch_timestamp TIMESTAMP,
-  -- Duration of `EventLatency`'s `LatchToSwapEnd` step.
-  viz_latch_to_swap_end_dur DURATION,
-  -- Timestamp for `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
-  swap_end_timestamp TIMESTAMP,
-  -- Duration of `EventLatency`'s `SwapEndToPresentationCompositorFrame` step.
-  swap_end_to_presentation_dur DURATION,
+  -- Duration of either `EventLatency`'s `LatchToSwapEnd` +
+  -- `SwapEndToPresentationCompositorFrame` steps or its `LatchToPresentation`
+  -- step.
+  viz_latch_to_presentation_dur DURATION,
   -- Presentation timestamp for the frame.
   presentation_timestamp TIMESTAMP)
 AS
@@ -961,12 +958,123 @@
   frame.viz_swap_buffers_to_latch_dur,
   -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   frame.latch_timestamp,
-  frame.viz_latch_to_swap_end_dur,
-  -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-  frame.swap_end_timestamp,
-  frame.swap_end_to_presentation_dur,
+  frame.viz_latch_to_presentation_dur,
   -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   frame.presentation_timestamp
 FROM chrome_scroll_update_input_info AS input
 LEFT JOIN chrome_scroll_update_frame_info AS frame
 ON input.presented_in_frame_id = frame.id;
+
+-- Source of truth for the definition of the stages of a scroll. Mainly intended
+-- for visualization purposes (e.g. in Chrome Scroll Jank plugin).
+CREATE PERFETTO TABLE chrome_scroll_update_info_step_templates(
+  -- The name of a stage of a scroll.
+  step_name STRING,
+  -- The name of the column in `chrome_scroll_update_info` which contains the
+  -- timestamp of the stage.
+  ts_column_name STRING,
+  -- The name of the column in `chrome_scroll_update_info` which contains the
+  -- duration of the stage. NULL if the stage doesn't have a duration.
+  dur_column_name STRING
+) AS
+WITH steps(step_name, ts_column_name, dur_column_name)
+AS (
+  VALUES
+  (
+    'GenerationToBrowserMain',
+    'generation_ts',
+    'generation_to_browser_main_dur'
+  ),
+  (
+    'TouchMoveProcessing',
+    'touch_move_received_ts',
+    'touch_move_processing_dur'
+  ),
+  (
+    'ScrollUpdateProcessing',
+    'scroll_update_created_ts',
+    'scroll_update_processing_dur'
+  ),
+  (
+    'BrowserMainToRendererCompositor',
+    'scroll_update_created_end_ts',
+    'browser_to_compositor_delay_dur'
+  ),
+  (
+    'RendererCompositorDispatch',
+    'compositor_dispatch_ts',
+    'compositor_dispatch_dur'
+  ),
+  (
+    'RendererCompositorDispatchToOnBeginFrame',
+    'compositor_dispatch_end_ts',
+    'compositor_dispatch_to_on_begin_frame_delay_dur'
+  ),
+  (
+    'RendererCompositorBeginFrame',
+    'compositor_on_begin_frame_ts',
+    'compositor_on_begin_frame_dur'
+  ),
+  (
+    'RendererCompositorBeginToGenerateFrame',
+    'compositor_on_begin_frame_end_ts',
+    'compositor_on_begin_frame_to_generation_delay_dur'
+  ),
+  (
+    'RendererCompositorGenerateToSubmitFrame',
+    'compositor_generate_compositor_frame_ts',
+    'compositor_generate_frame_to_submit_frame_dur'
+  ),
+  (
+    'RendererCompositorSubmitFrame',
+    'compositor_submit_compositor_frame_ts',
+    'compositor_submit_frame_dur'
+  ),
+  (
+    'RendererCompositorToViz',
+    'compositor_submit_compositor_frame_end_ts',
+    'compositor_to_viz_delay_dur'
+  ),
+  (
+    'VizReceiveFrame',
+    'viz_receive_compositor_frame_ts',
+    'viz_receive_compositor_frame_dur'
+  ),
+  (
+    'VizReceiveToDrawFrame',
+    'viz_receive_compositor_frame_end_ts',
+    'viz_wait_for_draw_dur'
+  ),
+  (
+    'VizDrawToSwapFrame',
+    'viz_draw_and_swap_ts',
+    'viz_draw_and_swap_dur'
+  ),
+  (
+    'VizToGpu',
+    'viz_send_buffer_swap_end_ts',
+    'viz_to_gpu_delay_dur'
+  ),
+  (
+    'VizSwapBuffers',
+    'viz_swap_buffers_ts',
+    'viz_swap_buffers_dur'
+  ),
+  (
+    'VizSwapBuffersToLatch',
+    'viz_swap_buffers_end_ts',
+    'viz_swap_buffers_to_latch_dur'
+  ),
+  (
+    'VizLatchToPresentation',
+    'latch_timestamp',
+    'viz_latch_to_presentation_dur'
+  ),
+  (
+    'Presentation',
+    'presentation_timestamp',
+    NULL
+  )
+)
+SELECT step_name, ts_column_name, dur_column_name
+FROM steps;
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
index d2e6330..82049b0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
@@ -99,7 +99,8 @@
   buffer_available_timestamp LONG,
   -- Timestamp of the BufferReadyToLatch substage.
   buffer_ready_timestamp LONG,
-  -- Timestamp of the LatchToSwapEnd substage.
+  -- Timestamp of the LatchToSwapEnd substage (or LatchToPresentation as a
+  -- fallback).
   latch_timestamp LONG,
   -- Timestamp of the SwapEndToPresentationCompositorFrame substage.
   swap_end_timestamp LONG,
@@ -129,7 +130,10 @@
     AS buffer_available_timestamp,
   _descendant_slice_begin(slice.id, 'BufferReadyToLatch')
     AS buffer_ready_timestamp,
-  _descendant_slice_begin(slice.id, 'LatchToSwapEnd') AS latch_timestamp,
+  COALESCE(
+    _descendant_slice_begin(slice.id, 'LatchToSwapEnd'),
+    _descendant_slice_begin(slice.id, 'LatchToPresentation')
+  ) AS latch_timestamp,
   _descendant_slice_begin(slice.id, 'SwapEndToPresentationCompositorFrame')
     AS swap_end_timestamp,
   _get_presentation_timestamp(slice.id) AS presentation_timestamp
diff --git a/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql b/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
index 5daf24d..881f930 100644
--- a/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/pixel/camera.sql
@@ -21,7 +21,7 @@
 -- provides timing information for each processing stage.
 CREATE PERFETTO TABLE pixel_camera_frames(
   -- Unique identifier for this slice.
-  id JOINID(slice.id),
+  id ID(slice.id),
   -- Start timestamp of the slice.
   ts TIMESTAMP,
   -- Duration of the slice execution.
diff --git a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
index 859918f..9704052 100644
--- a/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/tables_views.sql
@@ -260,42 +260,6 @@
 FROM
   __intrinsic_thread_state;
 
--- Contains 'raw' events from the trace for some types of events. This table
--- only exists for debugging purposes and should not be relied on in production
--- usecases (i.e. metrics, standard library etc.)
-CREATE PERFETTO VIEW raw (
-  -- Unique identifier for this raw event.
-  id ID,
-  -- The timestamp of this event.
-  ts TIMESTAMP,
-  -- The name of the event. For ftrace events, this will be the ftrace event
-  -- name.
-  name STRING,
-  -- The CPU this event was emitted on (meaningful only in single machine
-  -- traces). For multi-machine, join with the `cpu` table on `ucpu` to get the
-  -- CPU identifier of each machine.
-  cpu LONG,
-  -- The thread this event was emitted on.
-  utid JOINID(thread.id),
-  -- The set of key/value pairs associated with this event.
-  arg_set_id ARGSETID,
-  -- Ftrace event flags for this event. Currently only emitted for sched_waking
-  -- events.
-  common_flags LONG,
-  -- The unique CPU identifier that this event was emitted on.
-  ucpu LONG
-) AS
-SELECT
-  id,
-  ts,
-  name,
-  ucpu AS cpu,
-  utid,
-  arg_set_id,
-  common_flags,
-  ucpu
-FROM
-  __intrinsic_raw;
 
 -- Contains all the ftrace events in the trace. This table exists only for
 -- debugging purposes and should not be relied on in production usecases (i.e.
@@ -334,51 +298,37 @@
 FROM
   __intrinsic_ftrace_event;
 
--- The sched_slice table with the upid column.
-CREATE PERFETTO VIEW experimental_sched_upid (
-  -- Unique identifier for this scheduling slice.
+-- This table is deprecated. Use `ftrace_event` instead which contains the same
+-- rows; this table is simply a (badly named) alias.
+CREATE PERFETTO VIEW raw (
+  -- Unique identifier for this raw event.
   id ID,
-  -- The timestamp at the start of the slice.
+  -- The timestamp of this event.
   ts TIMESTAMP,
-  -- The duration of the slice.
-  dur DURATION,
-  -- The CPU that the slice executed on (meaningful only in single machine
+  -- The name of the event. For ftrace events, this will be the ftrace event
+  -- name.
+  name STRING,
+  -- The CPU this event was emitted on (meaningful only in single machine
   -- traces). For multi-machine, join with the `cpu` table on `ucpu` to get the
   -- CPU identifier of each machine.
   cpu LONG,
-  -- The thread's unique id in the trace.
+  -- The thread this event was emitted on.
   utid JOINID(thread.id),
-  -- A string representing the scheduling state of the kernel thread at the end
-  -- of the slice. The individual characters in the string mean the following: R
-  -- (runnable), S (awaiting a wakeup), D (in an uninterruptible sleep), T
-  -- (suspended), t (being traced), X (exiting), P (parked), W (waking), I
-  -- (idle), N (not contributing to the load average), K (wakeable on fatal
-  -- signals) and Z (zombie, awaiting cleanup).
-  end_state STRING,
-  -- The kernel priority that the thread ran at.
-  priority LONG,
-  -- The unique CPU identifier that the slice executed on.
-  ucpu LONG,
-  -- The process's unique id in the trace.
-  upid JOINID(process.id)
+  -- The set of key/value pairs associated with this event.
+  arg_set_id ARGSETID,
+  -- Ftrace event flags for this event. Currently only emitted for sched_waking
+  -- events.
+  common_flags LONG,
+  -- The unique CPU identifier that this event was emitted on.
+  ucpu LONG
 ) AS
-SELECT
-  id,
-  ts,
-  dur,
-  ucpu AS cpu,
-  utid,
-  end_state,
-  priority,
-  ucpu,
-  upid
-FROM
-  __intrinsic_sched_upid;
+SELECT *
+FROM ftrace_event;
 
 -- Tracks which are associated to a single thread.
 CREATE PERFETTO TABLE thread_track (
   -- Unique identifier for this thread track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -415,7 +365,7 @@
 -- Tracks which are associated to a single process.
 CREATE PERFETTO TABLE process_track (
   -- Unique identifier for this process track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -427,7 +377,7 @@
   type STRING,
   -- The track which is the "parent" of this track. Only non-null for tracks
   -- created using Perfetto's track_event API.
-  parent_id LONG,
+  parent_id JOINID(track.id),
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
@@ -452,7 +402,7 @@
 -- Tracks which are associated to a single CPU.
 CREATE PERFETTO TABLE cpu_track (
   -- Unique identifier for this cpu track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -494,7 +444,7 @@
 -- instead.
 CREATE PERFETTO TABLE gpu_track (
   -- Unique identifier for this cpu track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -506,7 +456,7 @@
   type STRING,
   -- The track which is the "parent" of this track. Only non-null for tracks
   -- created using Perfetto's track_event API.
-  parent_id LONG,
+  parent_id JOINID(track.id),
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
@@ -550,7 +500,7 @@
 -- Tracks containing counter-like events.
 CREATE PERFETTO VIEW counter_track (
   -- Unique identifier for this cpu counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The track which is the "parent" of this track. Only non-null for tracks
@@ -593,7 +543,7 @@
 -- Tracks containing counter-like events associated to a CPU.
 CREATE PERFETTO TABLE cpu_counter_track (
   -- Unique identifier for this cpu counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -636,7 +586,7 @@
 -- Tracks containing counter-like events associated to a GPU.
 CREATE PERFETTO TABLE gpu_counter_track (
   -- Unique identifier for this gpu counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -652,7 +602,7 @@
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
-  source_arg_set_id LONG,
+  source_arg_set_id ARGSETID,
   -- Machine identifier, non-null for tracks on a remote machine.
   machine_id LONG,
   -- The units of the counter. This column is rarely filled.
@@ -679,7 +629,7 @@
 -- Tracks containing counter-like events associated to a process.
 CREATE PERFETTO TABLE process_counter_track (
   -- Unique identifier for this process counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -695,7 +645,7 @@
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
-  source_arg_set_id LONG,
+  source_arg_set_id ARGSETID,
   -- Machine identifier, non-null for tracks on a remote machine.
   machine_id LONG,
   -- The units of the counter. This column is rarely filled.
@@ -722,7 +672,7 @@
 -- Tracks containing counter-like events associated to a thread.
 CREATE PERFETTO TABLE thread_counter_track (
   -- Unique identifier for this thread counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -738,7 +688,7 @@
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
-  source_arg_set_id LONG,
+  source_arg_set_id JOINID(track.id),
   -- Machine identifier, non-null for tracks on a remote machine.
   machine_id LONG,
   -- The units of the counter. This column is rarely filled.
@@ -765,7 +715,7 @@
 -- Tracks containing counter-like events collected from Linux perf.
 CREATE PERFETTO TABLE perf_counter_track (
   -- Unique identifier for this thread counter track.
-  id ID,
+  id ID(track.id),
   -- Name of the track.
   name STRING,
   -- The type of a track indicates the type of data the track contains.
@@ -781,7 +731,7 @@
   -- Args for this track which store information about "source" of this track in
   -- the trace. For example: whether this track orginated from atrace, Chrome
   -- tracepoints etc.
-  source_arg_set_id LONG,
+  source_arg_set_id ARGSETID,
   -- Machine identifier, non-null for tracks on a remote machine.
   machine_id LONG,
   -- The units of the counter. This column is rarely filled.
@@ -831,3 +781,243 @@
 FROM counter v
 JOIN counter_track t ON v.track_id = t.id
 ORDER BY ts;
+
+-- Table containing graphics frame events on Android.
+CREATE PERFETTO VIEW frame_slice(
+  -- Alias of `slice.id`.
+  id ID(slice.id),
+  -- Alias of `slice.ts`.
+  ts TIMESTAMP,
+  -- Alias of `slice.dur`.
+  dur DURATION,
+  -- Alias of `slice.track_id`.
+  track_id JOINID(track.id),
+  -- Alias of `slice.category`.
+  category STRING,
+  -- Alias of `slice.name`.
+  name STRING,
+  -- Alias of `slice.depth`.
+  depth LONG,
+  -- Alias of `slice.parent_id`.
+  parent_id JOINID(frame_slice.id),
+  -- Alias of `slice.arg_set_id`.
+  arg_set_id LONG,
+  -- Name of the graphics layer this slice happened on.
+  layer_name STRING,
+  -- The frame number this slice is associated with.
+  frame_number LONG,
+  -- The time between queue and acquire for this buffer and layer.
+  queue_to_acquire_time LONG,
+  -- The time between acquire and latch for this buffer and layer.
+  acquire_to_latch_time LONG,
+  -- The time between latch and present for this buffer and layer.
+  latch_to_present_time LONG
+) AS
+SELECT
+  s.id,
+  s.ts,
+  s.dur,
+  s.track_id,
+  s.category,
+  s.name,
+  s.depth,
+  s.parent_id,
+  s.arg_set_id,
+  extract_arg(s.arg_set_id, 'layer_name') as layer_name,
+  extract_arg(s.arg_set_id, 'frame_number') as frame_number,
+  extract_arg(s.arg_set_id, 'queue_to_acquire_time') as queue_to_acquire_time,
+  extract_arg(s.arg_set_id, 'acquire_to_latch_time') as acquire_to_latch_time,
+  extract_arg(s.arg_set_id, 'latch_to_present_time') as latch_to_present_time
+FROM slice s
+JOIN track t ON s.track_id = t.id
+WHERE t.type = 'graphics_frame_event';
+
+-- Table containing graphics frame events on Android.
+CREATE PERFETTO VIEW gpu_slice(
+  -- Alias of `slice.id`.
+  id ID(slice.id),
+  -- Alias of `slice.ts`.
+  ts TIMESTAMP,
+  -- Alias of `slice.dur`.
+  dur DURATION,
+  -- Alias of `slice.track_id`.
+  track_id JOINID(track.id),
+  -- Alias of `slice.category`.
+  category STRING,
+  -- Alias of `slice.name`.
+  name STRING,
+  -- Alias of `slice.depth`.
+  depth LONG,
+  -- Alias of `slice.parent_id`.
+  parent_id JOINID(frame_slice.id),
+  -- Alias of `slice.arg_set_id`.
+  arg_set_id LONG,
+  -- Context ID.
+  context_id LONG,
+  -- Render target ID.
+  render_target LONG,
+  -- The name of the render target.
+  render_target_name STRING,
+  -- Render pass ID.
+  render_pass LONG,
+  -- The name of the render pass.
+  render_pass_name STRING,
+  -- The command buffer ID.
+  command_buffer LONG,
+  -- The name of the command buffer.
+  command_buffer_name STRING,
+  -- Frame id.
+  frame_id LONG,
+  -- The submission id.
+  submission_id LONG,
+  -- The hardware queue id.
+  hw_queue_id LONG,
+  -- The id of the process.
+  upid JOINID(process.id),
+  -- Render subpasses.
+  render_subpasses STRING
+) AS
+SELECT
+  s.id,
+  s.ts,
+  s.dur,
+  s.track_id,
+  s.category,
+  s.name,
+  s.depth,
+  s.parent_id,
+  s.arg_set_id,
+  extract_arg(s.arg_set_id, 'context_id') as context_id,
+  extract_arg(s.arg_set_id, 'render_target') as render_target,
+  extract_arg(s.arg_set_id, 'render_target_name') as render_target_name,
+  extract_arg(s.arg_set_id, 'render_pass') as render_pass,
+  extract_arg(s.arg_set_id, 'render_pass_name') as render_pass_name,
+  extract_arg(s.arg_set_id, 'command_buffer') as command_buffer,
+  extract_arg(s.arg_set_id, 'command_buffer_name') as command_buffer_name,
+  extract_arg(s.arg_set_id, 'frame_id') as frame_id,
+  extract_arg(s.arg_set_id, 'submission_id') as submission_id,
+  extract_arg(s.arg_set_id, 'hw_queue_id') as hw_queue_id,
+  extract_arg(s.arg_set_id, 'upid') as upid,
+  extract_arg(s.arg_set_id, 'render_subpasses') as render_subpasses
+FROM slice s
+JOIN track t ON s.track_id = t.id
+WHERE t.type IN ('gpu_render_stage', 'vulkan_events', 'gpu_log');
+
+-- This table contains information on the expected timeline of either a display
+-- frame or a surface frame.
+CREATE PERFETTO TABLE expected_frame_timeline_slice(
+  -- Alias of `slice.id`.
+  id ID(slice.id),
+  -- Alias of `slice.ts`.
+  ts TIMESTAMP,
+  -- Alias of `slice.dur`.
+  dur DURATION,
+  -- Alias of `slice.track_id`.
+  track_id JOINID(track.id),
+  -- Alias of `slice.category`.
+  category STRING,
+  -- Alias of `slice.name`.
+  name STRING,
+  -- Alias of `slice.depth`.
+  depth LONG,
+  -- Alias of `slice.parent_id`.
+  parent_id JOINID(frame_slice.id),
+  -- Alias of `slice.arg_set_id`.
+  arg_set_id LONG,
+  -- Display frame token (vsync id).
+  display_frame_token LONG,
+  -- Surface frame token (vsync id), null if this is a display frame.
+  surface_frame_token LONG,
+  -- Unique process id of the app that generates the surface frame.
+  upid JOINID(process.id),
+  -- Layer name if this is a surface frame.
+  layer_name STRING
+) AS
+SELECT
+  s.id,
+  s.ts,
+  s.dur,
+  s.track_id,
+  s.category,
+  s.name,
+  s.depth,
+  s.parent_id,
+  s.arg_set_id,
+  extract_arg(s.arg_set_id, 'Display frame token') as display_frame_token,
+  extract_arg(s.arg_set_id, 'Surface frame token') as surface_frame_token,
+  t.upid,
+  extract_arg(s.arg_set_id, 'Layer name') as layer_name
+FROM slice s
+JOIN process_track t ON s.track_id = t.id
+WHERE t.type = 'android_expected_frame_timeline';
+
+-- This table contains information on the actual timeline and additional
+-- analysis related to the performance of either a display frame or a surface
+-- frame.
+CREATE PERFETTO TABLE actual_frame_timeline_slice(
+  -- Alias of `slice.id`.
+  id ID(slice.id),
+  -- Alias of `slice.ts`.
+  ts TIMESTAMP,
+  -- Alias of `slice.dur`.
+  dur DURATION,
+  -- Alias of `slice.track_id`.
+  track_id JOINID(track.id),
+  -- Alias of `slice.category`.
+  category STRING,
+  -- Alias of `slice.name`.
+  name STRING,
+  -- Alias of `slice.depth`.
+  depth LONG,
+  -- Alias of `slice.parent_id`.
+  parent_id JOINID(frame_slice.id),
+  -- Alias of `slice.arg_set_id`.
+  arg_set_id LONG,
+  -- Display frame token (vsync id).
+  display_frame_token LONG,
+  -- Surface frame token (vsync id), null if this is a display frame.
+  surface_frame_token LONG,
+  -- Unique process id of the app that generates the surface frame.
+  upid JOINID(process.id),
+  -- Layer name if this is a surface frame.
+  layer_name STRING,
+  -- Frame's present type (eg. on time / early / late).
+  present_type STRING,
+  -- Whether the frame finishes on time.
+  on_time_finish LONG,
+  -- Whether the frame used gpu composition.
+  gpu_composition LONG,
+  -- Specify the jank types for this frame if there's jank, or none if no jank
+  -- occurred.
+  jank_type STRING,
+  -- Severity of the jank: none if no jank.
+  jank_severity_type STRING,
+  -- Frame's prediction type (eg. valid / expired).
+  prediction_type STRING,
+  -- Jank tag based on jank type, used for slice visualization.
+  jank_tag STRING
+) AS
+SELECT
+  s.id,
+  s.ts,
+  s.dur,
+  s.track_id,
+  s.category,
+  s.name,
+  s.depth,
+  s.parent_id,
+  s.arg_set_id,
+  extract_arg(s.arg_set_id, 'Display frame token') as display_frame_token,
+  extract_arg(s.arg_set_id, 'Surface frame token') as surface_frame_token,
+  t.upid,
+  extract_arg(s.arg_set_id, 'Layer name') as layer_name,
+  extract_arg(s.arg_set_id, 'Present type') as present_type,
+  extract_arg(s.arg_set_id, 'On time finish') as on_time_finish,
+  extract_arg(s.arg_set_id, 'GPU composition') as gpu_composition,
+  extract_arg(s.arg_set_id, 'Jank type') as jank_type,
+  extract_arg(s.arg_set_id, 'Jank severity type') as jank_severity_type,
+  extract_arg(s.arg_set_id, 'Prediction type') as prediction_type,
+  extract_arg(s.arg_set_id, 'Jank tag') as jank_tag
+FROM slice s
+JOIN process_track t ON s.track_id = t.id
+WHERE t.type = 'android_actual_frame_timeline';
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql b/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
index a1cd9d6..e38ac97 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/cpu_time.sql
@@ -34,13 +34,13 @@
   -- Duration of the time the slice was running.
   cpu_time LONG) AS
 SELECT
-id_0 AS id,
-name,
-ts.utid,
-thread_name,
-upid,
-process_name,
-SUM(ii.dur) AS cpu_time
+  id_0 AS id,
+  name,
+  ts.utid,
+  thread_name,
+  upid,
+  process_name,
+  SUM(ii.dur) AS cpu_time
 FROM _interval_intersect!((
   (SELECT * FROM thread_slice WHERE utid > 0 AND dur > 0),
   (SELECT * FROM sched WHERE dur > 0)
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 78a7e5d..aafdef0 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql
@@ -17,7 +17,7 @@
 -- Where possible, use available view functions which filter this view.
 CREATE PERFETTO VIEW thread_slice(
   -- Slice
-  id JOINID(slice.id),
+  id ID(slice.id),
   -- Alias for `slice.ts`.
   ts TIMESTAMP,
   -- Alias for `slice.dur`.
@@ -84,7 +84,7 @@
 -- Where possible, use available view functions which filter this view.
 CREATE PERFETTO VIEW process_slice(
   -- Slice
-  id JOINID(slice.id),
+  id ID(slice.id),
   -- Alias for `slice.ts`.
   ts TIMESTAMP,
   -- Alias for `slice.dur`.
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
index 3c2356f..5ce76ff 100644
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/BUILD.gn
@@ -22,6 +22,6 @@
     "threads.sql",
     "threads_w_processes.sql",
     "trace.sql",
-    "tracks.sql",
+    "track_event.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql
new file mode 100644
index 0000000..c59052e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/track_event.sql
@@ -0,0 +1,111 @@
+--
+-- 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 _track_event_tracks_unordered AS
+WITH extracted AS (
+  SELECT
+    t.id,
+    t.name,
+    t.parent_id,
+    EXTRACT_ARG(t.source_arg_set_id, 'child_ordering') AS ordering,
+    EXTRACT_ARG(t.source_arg_set_id, 'sibling_order_rank') AS rank
+  FROM track t
+  WHERE t.type GLOB '*_track_event'
+)
+SELECT
+  t.id,
+  t.name,
+  t.parent_id,
+  p.ordering AS parent_ordering,
+  IFNULL(t.rank, 0) AS rank
+FROM extracted t
+LEFT JOIN extracted p ON t.parent_id = p.id;
+
+CREATE PERFETTO TABLE _min_ts_per_track AS
+SELECT track_id AS id, min(ts) as min_ts
+FROM counter
+GROUP BY track_id
+UNION ALL
+SELECT track_id AS id, min(ts) as min_ts
+FROM slice
+GROUP BY track_id;
+
+CREATE PERFETTO TABLE _track_event_has_children AS
+SELECT DISTINCT t.parent_id AS id
+FROM track t
+WHERE t.type GLOB '*_track_event' AND t.parent_id IS NOT NULL;
+
+CREATE PERFETTO TABLE _track_event_tracks_ordered_groups AS
+WITH
+  lexicographic_and_none AS (
+    SELECT
+      id,
+      ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY name) AS order_id
+    FROM _track_event_tracks_unordered t
+    WHERE t.parent_ordering = 'lexicographic'
+      OR t.parent_ordering IS NULL
+  ),
+  explicit AS (
+    SELECT
+      id,
+      ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY rank) AS order_id
+    FROM _track_event_tracks_unordered t
+    WHERE t.parent_ordering = 'explicit'
+  ),
+  chronological AS (
+    SELECT
+      t.id,
+      ROW_NUMBER() OVER (PARTITION BY t.parent_id ORDER BY m.min_ts) AS order_id
+    FROM _track_event_tracks_unordered t
+    LEFT JOIN _min_ts_per_track m USING (id)
+    WHERE t.parent_ordering = 'chronological'
+  ),
+  unioned AS (
+    SELECT id, order_id
+    FROM lexicographic_and_none
+    UNION ALL
+    SELECT id, order_id
+    FROM explicit
+    UNION ALL
+    SELECT id, order_id
+    FROM chronological
+  )
+SELECT
+  extract_arg(track.dimension_arg_set_id, 'upid') AS upid,
+  extract_arg(track.dimension_arg_set_id, 'utid') AS utid,
+  track.parent_id,
+  track.type GLOB '*counter*' AS is_counter,
+  track.name,
+  MIN(counter_track.unit) AS unit,
+  MIN(extract_arg(track.source_arg_set_id, 'builtin_counter_type')) AS builtin_counter_type,
+  MAX(m.id IS NOT NULL) AS has_data,
+  MAX(c.id IS NOT NULL) AS has_children,
+  GROUP_CONCAT(unioned.id) as track_ids,
+  MIN(unioned.order_id) AS order_id
+FROM unioned
+JOIN track USING (id)
+LEFT JOIN counter_track USING (id)
+LEFT JOIN _track_event_has_children c USING (id)
+LEFT JOIN _min_ts_per_track m USING (id)
+GROUP BY
+  -- Merge by parent id if it exists or, if not, then by upid/utid scope.
+  coalesce(track.parent_id, upid, utid),
+  is_counter,
+  track.name,
+  -- Don't merge tracks by name which have children or are counters.
+  IIF(c.id IS NOT NULL OR is_counter, track.id, NULL)
+ORDER BY track.parent_id, unioned.order_id;
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
deleted file mode 100644
index 49fb3f0..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
+++ /dev/null
@@ -1,128 +0,0 @@
---
--- 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 VIEW _track_event_tracks_unordered AS
-WITH extracted AS (
-  SELECT
-    t.id,
-    t.parent_id,
-    t.name,
-    EXTRACT_ARG(t.source_arg_set_id, 'child_ordering') AS ordering,
-    EXTRACT_ARG(t.source_arg_set_id, 'sibling_order_rank') AS rank
-  FROM track t
-)
-SELECT
-  t.id,
-  t.parent_id,
-  t.name,
-  t.ordering,
-  p.ordering AS parent_ordering,
-  IFNULL(t.rank, 0) AS rank
-FROM extracted t
-LEFT JOIN extracted p ON t.parent_id = p.id
-WHERE p.ordering IS NOT NULL;
-
-CREATE PERFETTO TABLE _track_event_tracks_ordered AS
-WITH lexicographic_and_none AS (
-  SELECT
-    id, parent_id, name,
-    ROW_NUMBER() OVER (ORDER BY parent_id, name) AS order_id
-  FROM _track_event_tracks_unordered
-  WHERE parent_ordering = 'lexicographic'
-),
-explicit AS (
-SELECT
-  id, parent_id, name,
-  ROW_NUMBER() OVER (ORDER BY parent_id, rank) AS order_id
-FROM _track_event_tracks_unordered
-WHERE parent_ordering = 'explicit'
-),
-slice_chronological AS (
-  SELECT
-    t.*,
-    min(ts) AS min_ts
-  FROM _track_event_tracks_unordered t
-  JOIN slice s on t.id = s.track_id
-  WHERE parent_ordering = 'chronological'
-  GROUP BY track_id
-),
-counter_chronological AS (
-  SELECT
-    t.*,
-    min(ts) AS min_ts
-  FROM _track_event_tracks_unordered t
-  JOIN counter s on t.id = s.track_id
-  WHERE parent_ordering = 'chronological'
-  GROUP BY track_id
-),
-slice_and_counter_chronological AS (
-  SELECT t.*, u.min_ts
-  FROM _track_event_tracks_unordered t
-  LEFT JOIN (
-    SELECT * FROM slice_chronological
-    UNION ALL
-    SELECT * FROM counter_chronological) u USING (id)
-  WHERE t.parent_ordering = 'chronological'
-),
-chronological AS (
-  SELECT
-    id, parent_id, name,
-    ROW_NUMBER() OVER (ORDER BY parent_id, min_ts) AS order_id
-  FROM slice_and_counter_chronological
-),
-all_tracks AS (
-  SELECT id, parent_id, name, order_id
-  FROM lexicographic_and_none
-  UNION
-  SELECT id, parent_id, name, order_id
-  FROM explicit
-  UNION
-  SELECT id, parent_id, name, order_id
-  FROM chronological
-)
-SELECT id, order_id
-FROM all_tracks all_t
-ORDER BY parent_id, order_id;
-
-CREATE PERFETTO TABLE _thread_track_summary_by_utid_and_name AS
-SELECT
-  utid,
-  parent_id,
-  name,
-  -- Only meaningful when track_count == 1.
-  id as track_id,
-  -- Only meaningful when track_count == 1.
-  max_depth as max_depth,
-  GROUP_CONCAT(id) AS track_ids,
-  COUNT() AS track_count
-FROM thread_track
-JOIN _slice_track_summary USING (id)
-LEFT JOIN _track_event_tracks_ordered USING (id)
-GROUP BY utid, parent_id, order_id, name;
-
-CREATE PERFETTO TABLE _process_track_summary_by_upid_and_parent_id_and_name AS
-SELECT
-  id,
-  parent_id,
-  upid,
-  name,
-  GROUP_CONCAT(id) AS track_ids,
-  COUNT() AS track_count
-FROM process_track
-JOIN _slice_track_summary USING (id)
-LEFT JOIN _track_event_tracks_ordered USING (id)
-GROUP BY upid, parent_id, order_id, name;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
index 185d6ef..e32094c 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/BUILD.gn
@@ -19,6 +19,7 @@
     "arm_dsu.sql",
     "cpu_freq.sql",
     "cpu_freq_idle.sql",
+    "cpu_hotplug.sql",
     "cpu_idle.sql",
     "cpu_split.sql",
     "curves/device.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
index 1256f3b..c211e8e 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
@@ -30,9 +30,9 @@
   ),
   -- Get first freq transition per CPU
   first_cpu_freq_slices AS (
-    SELECT ts, cpu FROM _cpu_freq
+    SELECT MIN(ts) as ts, cpu
+    FROM _cpu_freq
     GROUP BY cpu
-    ORDER by ts ASC
   )
 -- Prepend NULL slices up to first freq events on a per CPU basis
 SELECT
@@ -44,6 +44,7 @@
   d_map.policy
 FROM first_cpu_freq_slices as first_slices
 JOIN _dev_cpu_policy_map as d_map ON first_slices.cpu = d_map.cpu
+WHERE dur > 0
 UNION ALL
 SELECT
   ts,
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
index 756ebed..dba6cca 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq_idle.sql
@@ -15,6 +15,7 @@
 
 INCLUDE PERFETTO MODULE intervals.intersect;
 INCLUDE PERFETTO MODULE wattson.cpu_freq;
+INCLUDE PERFETTO MODULE wattson.cpu_hotplug;
 INCLUDE PERFETTO MODULE wattson.cpu_idle;
 INCLUDE PERFETTO MODULE wattson.curves.utils;
 INCLUDE PERFETTO MODULE wattson.device_infos;
@@ -57,17 +58,31 @@
 CREATE PERFETTO TABLE _idle_freq_materialized
 AS
 SELECT
-  ii.ts, ii.dur, ii.cpu, freq.policy, freq.freq, idle.idle, lut.curve_value
+  ii.ts, ii.dur, ii.cpu, freq.policy, freq.freq,
+  -- Set idle since subsequent calculations are based on number of idle/active
+  -- CPUs. If offline/suspended, set the CPU to the device specific deepest idle
+  -- state.
+  IIF(
+    suspend.suspended OR hotplug.offline,
+    (SELECT idle FROM _deepest_idle),
+    idle.idle
+  ) as idle,
+  -- If CPU is suspended or offline, set power estimate to 0
+  IIF(suspend.suspended OR hotplug.offline, 0, lut.curve_value) as curve_value
 FROM _interval_intersect!(
   (
     _ii_subquery!(_valid_window),
     _ii_subquery!(_adjusted_cpu_freq),
-    _ii_subquery!(_adjusted_deep_idle)
+    _ii_subquery!(_adjusted_deep_idle),
+    _ii_subquery!(_gapless_hotplug_slices),
+    _ii_subquery!(_gapless_suspend_slices)
   ),
   (cpu)
 ) ii
 JOIN _adjusted_cpu_freq AS freq ON freq._auto_id = id_1
 JOIN _adjusted_deep_idle AS idle ON idle._auto_id = id_2
+JOIN _gapless_hotplug_slices AS hotplug ON hotplug._auto_id = id_3
+JOIN _gapless_suspend_slices AS suspend ON suspend._auto_id = id_4
 -- Left join since some CPUs may only match the 2D LUT
 LEFT JOIN _filtered_curves_1d lut ON
   freq.policy = lut.policy AND
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql
new file mode 100644
index 0000000..283c28e
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_hotplug.sql
@@ -0,0 +1,108 @@
+--
+-- 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.suspend;
+INCLUDE PERFETTO MODULE time.conversion;
+INCLUDE PERFETTO MODULE wattson.device_infos;
+
+-- Creates the hotplug slice(s) for each CPU defined to be the region when CPU
+-- is off
+CREATE PERFETTO TABLE _cpu_hotplug_offline AS
+WITH is_different_cpu AS (
+  -- Set flag for when hotplug CPU processing is being done on separate CPU
+  SELECT
+    s.ts,
+    s.dur,
+    EXTRACT_ARG(t.dimension_arg_set_id, 'cpu') AS hp_cpu,
+    EXTRACT_ARG(t.dimension_arg_set_id, 'cpu')
+      != EXTRACT_ARG(s.arg_set_id, 'action_cpu') AS is_different_cpu
+  FROM slice s
+  JOIN track t ON t.id = s.track_id
+  WHERE t.type = 'cpu_hotplug'
+),
+cpu_transitions AS (
+  -- AP CPU is CPU being hotplugged out, and BP CPU is CPU that assists the AP
+  -- CPU in hotplugging. The BP CPU could be the AP CPU itself or a different
+  -- CPU. Find the transition points where the BP CPU changes between AP and BP.
+  SELECT
+    ts,
+    hp_cpu,
+    is_different_cpu,
+    LAG(is_different_cpu) OVER (PARTITION BY hp_cpu) != is_different_cpu
+      AS is_cpu_transitions
+  FROM is_different_cpu
+),
+transitions_dur AS (
+  -- Calculates duration between transitions from AP -> BP and BP -> AP
+  SELECT
+    ts,
+    LEAD(ts, 1, trace_end()) OVER (PARTITION BY hp_cpu) - ts AS dur,
+    hp_cpu AS cpu,
+    is_different_cpu
+  FROM cpu_transitions
+  WHERE is_cpu_transitions
+)
+SELECT
+  ts, dur, cpu,
+  -- Sometimes the assignment of AP CPU during hotplugging creates short,
+  -- spurious "pockets" of hotplug events, so assign these slices that are
+  -- shorter than 100us as if they were on the same CPU.
+  IIF(
+    is_different_cpu AND dur < time_from_us(100),
+    FALSE,
+    is_different_cpu
+  ) AS is_different_cpu
+FROM transitions_dur;
+
+-- Fill gaps from beginning of trace to end of trace so that this table can be
+-- used by interval_intersect().
+CREATE PERFETTO TABLE _gapless_hotplug_slices AS
+WITH filled_gaps AS (
+  -- First slice from trace_start() to first offline slice per CPU
+  SELECT
+    cpu,
+    trace_start() AS ts,
+    MIN(ts) - trace_start() AS dur,
+    FALSE AS offline
+  FROM _cpu_hotplug_offline
+  GROUP BY cpu
+  UNION ALL
+  -- All online and offline regions as defined by cpuhp. This will have
+  -- continuous slices from somewhere in the middle to the end of the trace.
+  SELECT
+    cpu, ts, dur, is_different_cpu AS offline
+  FROM _cpu_hotplug_offline
+  UNION ALL
+  -- Creates a single online slice spanning the entire trace for CPUs that are
+  -- never offline. This is needed for interval_intersect() to not delete
+  -- undefined time periods
+  SELECT
+    cpu,
+    trace_start() AS ts,
+    trace_dur() AS dur,
+    FALSE AS offline
+  FROM _dev_cpu_policy_map
+  WHERE cpu NOT IN (SELECT cpu FROM _cpu_hotplug_offline)
+)
+SELECT ts, dur, cpu, offline
+FROM filled_gaps
+ORDER BY cpu, ts;
+
+-- Copies suspend state to each CPU defined, so that the suspend state can be
+-- partitioned by cpu during interval_intersect()
+CREATE PERFETTO TABLE _gapless_suspend_slices AS
+SELECT cpu, ts, dur, IIF(power_state = 'suspended', TRUE, FALSE) AS suspended
+FROM _dev_cpu_policy_map
+CROSS JOIN android_suspend_state;
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
index a6941c8..e5a37d1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/device_infos.sql
@@ -195,3 +195,9 @@
 FROM _idle_state_map as idle_map
 JOIN _wattson_device as device
 ON idle_map.device = device.name;
+
+-- Get the device specific deepest idle state if defined, otherwise use 1 as the
+-- deepest idle state
+CREATE PERFETTO TABLE _deepest_idle AS
+SELECT
+  IFNULL((SELECT MAX(override_idle) FROM _idle_state_map_override), 1) as idle;
diff --git a/src/trace_processor/read_trace_integrationtest.cc b/src/trace_processor/read_trace_integrationtest.cc
index 818eb6d..1bb4084 100644
--- a/src/trace_processor/read_trace_integrationtest.cc
+++ b/src/trace_processor/read_trace_integrationtest.cc
@@ -69,7 +69,7 @@
   std::vector<uint8_t> decompressed;
   decompressed.reserve(raw_trace.size());
 
-  util::Status status = trace_processor::DecompressTrace(
+  base::Status status = trace_processor::DecompressTrace(
       raw_trace.data(), raw_trace.size(), &decompressed);
   ASSERT_TRUE(status.ok());
 
@@ -89,7 +89,7 @@
   std::vector<uint8_t> raw_trace = ReadAllData(f);
 
   std::vector<uint8_t> decompressed;
-  util::Status status = trace_processor::DecompressTrace(
+  base::Status status = trace_processor::DecompressTrace(
       raw_trace.data(), raw_trace.size(), &decompressed);
   ASSERT_FALSE(status.ok());
 }
@@ -100,7 +100,7 @@
   std::vector<uint8_t> raw_compressed_trace = ReadAllData(f);
 
   std::vector<uint8_t> decompressed;
-  util::Status status = trace_processor::DecompressTrace(
+  base::Status status = trace_processor::DecompressTrace(
       raw_compressed_trace.data(), raw_compressed_trace.size(), &decompressed);
   ASSERT_TRUE(status.ok());
 
@@ -117,7 +117,7 @@
   std::vector<uint8_t> raw_compressed_trace = ReadAllData(f);
 
   std::vector<uint8_t> decompressed;
-  util::Status status = trace_processor::DecompressTrace(
+  base::Status status = trace_processor::DecompressTrace(
       raw_compressed_trace.data(), raw_compressed_trace.size(), &decompressed);
   ASSERT_TRUE(status.ok()) << status.message();
 
diff --git a/src/trace_processor/read_trace_internal.cc b/src/trace_processor/read_trace_internal.cc
index f2b1a67..56bd11d 100644
--- a/src/trace_processor/read_trace_internal.cc
+++ b/src/trace_processor/read_trace_internal.cc
@@ -41,7 +41,7 @@
 // 1MB chunk size seems the best tradeoff on a MacBook Pro 2013 - i7 2.8 GHz.
 constexpr size_t kChunkSize = 1024 * 1024;
 
-util::Status ReadTraceUsingRead(
+base::Status ReadTraceUsingRead(
     TraceProcessor* tp,
     int fd,
     uint64_t* file_size,
@@ -57,7 +57,7 @@
       break;
 
     if (rsize < 0) {
-      return util::ErrStatus("Reading trace file failed (errno: %d, %s)", errno,
+      return base::ErrStatus("Reading trace file failed (errno: %d, %s)", errno,
                              strerror(errno));
     }
 
@@ -65,11 +65,11 @@
     TraceBlobView blob_view(std::move(blob), 0, static_cast<size_t>(rsize));
     RETURN_IF_ERROR(tp->Parse(std::move(blob_view)));
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 }  // namespace
 
-util::Status ReadTraceUnfinalized(
+base::Status ReadTraceUnfinalized(
     TraceProcessor* tp,
     const char* filename,
     const std::function<void(uint64_t parsed_size)>& progress_callback) {
@@ -102,7 +102,7 @@
   if (bytes_read == 0) {
     base::ScopedFile fd(base::OpenFile(filename, O_RDONLY));
     if (!fd)
-      return util::ErrStatus("Could not open trace file (path: %s)", filename);
+      return base::ErrStatus("Could not open trace file (path: %s)", filename);
     RETURN_IF_ERROR(
         ReadTraceUsingRead(tp, *fd, &bytes_read, progress_callback));
   }
@@ -110,7 +110,7 @@
 
   if (progress_callback)
     progress_callback(bytes_read);
-  return util::OkStatus();
+  return base::OkStatus();
 }
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/read_trace_internal.h b/src/trace_processor/read_trace_internal.h
index c946ad7..a54dc5c 100644
--- a/src/trace_processor/read_trace_internal.h
+++ b/src/trace_processor/read_trace_internal.h
@@ -30,7 +30,7 @@
 class TraceProcessor;
 
 // Reads trace without Flushing the data at the end.
-util::Status PERFETTO_EXPORT_COMPONENT ReadTraceUnfinalized(
+base::Status PERFETTO_EXPORT_COMPONENT ReadTraceUnfinalized(
     TraceProcessor* tp,
     const char* filename,
     const std::function<void(uint64_t parsed_size)>& progress_callback = {});
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index 9703efe..d7bb07f 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -244,11 +244,6 @@
       context.json_trace_parser->ParseJsonPacket(
           event.ts, std::move(token_buffer_.Extract<JsonEvent>(id).value));
       return;
-    case TimestampedEvent::Type::kJsonValueWithDur:
-      context.json_trace_parser->ParseJsonPacket(
-          event.ts,
-          std::move(token_buffer_.Extract<JsonWithDurEvent>(id).value));
-      return;
     case TimestampedEvent::Type::kSpeRecord:
       context.spe_record_parser->ParseSpeRecord(
           event.ts, token_buffer_.Extract<TraceBlobView>(id));
@@ -310,7 +305,6 @@
     case TimestampedEvent::Type::kPerfRecord:
     case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
-    case TimestampedEvent::Type::kJsonValueWithDur:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidDumpstateEvent:
     case TimestampedEvent::Type::kAndroidLogEvent:
@@ -348,7 +342,6 @@
     case TimestampedEvent::Type::kPerfRecord:
     case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
-    case TimestampedEvent::Type::kJsonValueWithDur:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidDumpstateEvent:
     case TimestampedEvent::Type::kAndroidLogEvent:
@@ -379,9 +372,6 @@
     case TimestampedEvent::Type::kJsonValue:
       base::ignore_result(token_buffer_.Extract<JsonEvent>(id));
       return;
-    case TimestampedEvent::Type::kJsonValueWithDur:
-      base::ignore_result(token_buffer_.Extract<JsonWithDurEvent>(id));
-      return;
     case TimestampedEvent::Type::kSpeRecord:
       base::ignore_result(token_buffer_.Extract<TraceBlobView>(id));
       return;
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 6727dcc..760f092 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -27,8 +27,10 @@
 #include <tuple>
 #include <type_traits>
 #include <utility>
+#include <variant>
 #include <vector>
 
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/circular_queue.h"
 #include "perfetto/public/compiler.h"
@@ -188,17 +190,15 @@
 
   void PushJsonValue(int64_t timestamp,
                      std::string json_value,
-                     std::optional<int64_t> dur = std::nullopt) {
-    if (dur.has_value()) {
-      // We need to account for slices with duration by sorting them first: this
-      // requires us to use the slower comparator which takes this into account.
+                     const JsonEvent::Type& type) {
+    if (const auto* scoped = std::get_if<JsonEvent::Scoped>(&type); scoped) {
+      // We need to account for slices with duration by sorting them specially:
+      // this requires us to use the slower comparator which takes this into
+      // account.
       use_slow_sorting_ = true;
-      AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kJsonValueWithDur,
-                           JsonWithDurEvent{*dur, std::move(json_value)});
-      return;
     }
     AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kJsonValue,
-                         JsonEvent{std::move(json_value)});
+                         JsonEvent{std::move(json_value), type});
   }
 
   void PushFuchsiaRecord(int64_t timestamp, FuchsiaRecord fuchsia_record) {
@@ -357,7 +357,6 @@
       kInlineSchedWaking,
       kInstrumentsRow,
       kJsonValue,
-      kJsonValueWithDur,
       kLegacyV8CpuProfileEvent,
       kPerfRecord,
       kSpeRecord,
@@ -410,18 +409,26 @@
       bool operator()(const TimestampedEvent& a,
                       const TimestampedEvent& b) const {
         int64_t a_key =
-            a.type() == Type::kJsonValueWithDur
-                ? std::numeric_limits<int64_t>::max() -
-                      buffer.Get<JsonWithDurEvent>(GetTokenBufferId(a))->dur
-                : std::numeric_limits<int64_t>::max();
+            KeyForType(buffer.Get<JsonEvent>(GetTokenBufferId(a))->type);
         int64_t b_key =
-            b.type() == Type::kJsonValueWithDur
-                ? std::numeric_limits<int64_t>::max() -
-                      buffer.Get<JsonWithDurEvent>(GetTokenBufferId(b))->dur
-                : std::numeric_limits<int64_t>::max();
+            KeyForType(buffer.Get<JsonEvent>(GetTokenBufferId(b))->type);
         return std::tie(a.ts, a_key, a.chunk_index, a.chunk_offset) <
                std::tie(b.ts, b_key, b.chunk_index, b.chunk_offset);
       }
+
+      static int64_t KeyForType(const JsonEvent::Type& type) {
+        switch (type.index()) {
+          case base::variant_index<JsonEvent::Type, JsonEvent::End>():
+            return std::numeric_limits<int64_t>::min();
+          case base::variant_index<JsonEvent::Type, JsonEvent::Scoped>():
+            return std::numeric_limits<int64_t>::max() -
+                   std::get<JsonEvent::Scoped>(type).dur;
+          default:
+            return std::numeric_limits<int64_t>::max();
+        }
+        PERFETTO_FATAL("For GCC");
+      }
+
       TraceTokenBuffer& buffer;
     };
   };
@@ -462,7 +469,6 @@
         // after that index, instead, will need a sorting pass before moving
         // events to the next pipeline stage.
         if (sort_start_idx_ == 0) {
-          PERFETTO_DCHECK(events_.size() >= 2);
           sort_start_idx_ = events_.size() - 1;
           sort_min_ts_ = ts;
         } else {
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index c119d99..3b8e5fd 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -678,6 +678,21 @@
   info->estimatedCost = cost_and_rows.cost;
   info->estimatedRows = cost_and_rows.rows;
 
+  PERFETTO_TP_TRACE(
+      metatrace::Category::QUERY_TIMELINE, "DB_SQLITE_BEST_INDEX",
+      [&](metatrace::Record* record) {
+        record->AddArg("name", t->table_name.c_str());
+        record->AddArg("idxStr", info->idxStr);
+        record->AddArg("idxNum",
+                       base::StackString<32>("%d", info->idxNum).c_str());
+        record->AddArg(
+            "estimatedCost",
+            base::StackString<32>("%f", info->estimatedCost).c_str());
+        record->AddArg(
+            "estimatedRows",
+            base::StackString<32>("%lld", info->estimatedRows).c_str());
+      });
+
   return SQLITE_OK;
 }
 
diff --git a/src/trace_processor/storage/metadata.h b/src/trace_processor/storage/metadata.h
index a690f84..10ed55d 100644
--- a/src/trace_processor/storage/metadata.h
+++ b/src/trace_processor/storage/metadata.h
@@ -65,7 +65,8 @@
   F(tracing_disabled_ns,               KeyType::kSingle,  Variadic::kInt),    \
   F(tracing_started_ns,                KeyType::kSingle,  Variadic::kInt),    \
   F(ui_state,                          KeyType::kSingle,  Variadic::kString), \
-  F(unique_session_name,               KeyType::kSingle,  Variadic::kString)
+  F(unique_session_name,               KeyType::kSingle,  Variadic::kString), \
+  F(trace_trigger,                     KeyType::kSingle,  Variadic::kString)
 // clang-format on
 
 // Compile time list of metadata items.
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 8b29070..0cc9ce0 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -81,7 +81,6 @@
 static const StringId kNullStringId = StringId::Null();
 
 using ArgSetId = uint32_t;
-static const ArgSetId kInvalidArgSetId = 0;
 
 using TrackId = tables::TrackTable::Id;
 
@@ -101,8 +100,6 @@
 
 using MetadataId = tables::MetadataTable::Id;
 
-using RawId = tables::RawTable::Id;
-
 using FlamegraphId = tables::ExperimentalFlamegraphTable::Id;
 
 using VulkanAllocId = tables::VulkanMemoryAllocationsTable::Id;
@@ -349,7 +346,7 @@
     track_table_.ShrinkToFit();
     counter_table_.ShrinkToFit();
     slice_table_.ShrinkToFit();
-    raw_table_.ShrinkToFit();
+    ftrace_event_table_.ShrinkToFit();
     sched_slice_table_.ShrinkToFit();
     thread_state_table_.ShrinkToFit();
     arg_table_.ShrinkToFit();
@@ -414,11 +411,6 @@
     return &virtual_track_slices_;
   }
 
-  const tables::GpuSliceTable& gpu_slice_table() const {
-    return gpu_slice_table_;
-  }
-  tables::GpuSliceTable* mutable_gpu_slice_table() { return &gpu_slice_table_; }
-
   const tables::CounterTable& counter_table() const { return counter_table_; }
   tables::CounterTable* mutable_counter_table() { return &counter_table_; }
 
@@ -480,8 +472,12 @@
   const tables::ArgTable& arg_table() const { return arg_table_; }
   tables::ArgTable* mutable_arg_table() { return &arg_table_; }
 
-  const tables::RawTable& raw_table() const { return raw_table_; }
-  tables::RawTable* mutable_raw_table() { return &raw_table_; }
+  const tables::ChromeRawTable& chrome_raw_table() const {
+    return chrome_raw_table_;
+  }
+  tables::ChromeRawTable* mutable_chrome_raw_table() {
+    return &chrome_raw_table_;
+  }
 
   const tables::FtraceEventTable& ftrace_event_table() const {
     return ftrace_event_table_;
@@ -625,14 +621,6 @@
     return &vulkan_memory_allocations_table_;
   }
 
-  const tables::GraphicsFrameSliceTable& graphics_frame_slice_table() const {
-    return graphics_frame_slice_table_;
-  }
-
-  tables::GraphicsFrameSliceTable* mutable_graphics_frame_slice_table() {
-    return &graphics_frame_slice_table_;
-  }
-
   const tables::MemorySnapshotTable& memory_snapshot_table() const {
     return memory_snapshot_table_;
   }
@@ -662,25 +650,6 @@
     return &memory_snapshot_edge_table_;
   }
 
-  const tables::ExpectedFrameTimelineSliceTable&
-  expected_frame_timeline_slice_table() const {
-    return expected_frame_timeline_slice_table_;
-  }
-
-  tables::ExpectedFrameTimelineSliceTable*
-  mutable_expected_frame_timeline_slice_table() {
-    return &expected_frame_timeline_slice_table_;
-  }
-
-  const tables::ActualFrameTimelineSliceTable&
-  actual_frame_timeline_slice_table() const {
-    return actual_frame_timeline_slice_table_;
-  }
-  tables::ActualFrameTimelineSliceTable*
-  mutable_actual_frame_timeline_slice_table() {
-    return &actual_frame_timeline_slice_table_;
-  }
-
   const tables::AndroidNetworkPacketsTable& android_network_packets_table()
       const {
     return android_network_packets_table_;
@@ -1042,18 +1011,14 @@
   // NestableSlices).
   VirtualTrackSlices virtual_track_slices_;
 
-  // Additional attributes for gpu track slices (sub-type of
-  // NestableSlices).
-  tables::GpuSliceTable gpu_slice_table_{&string_pool_, &slice_table_};
-
   // The values from the Counter events from the trace. This includes CPU
   // frequency events as well systrace trace_marker counter events.
   tables::CounterTable counter_table_{&string_pool_};
 
   SqlStats sql_stats_;
 
-  tables::RawTable raw_table_{&string_pool_};
-  tables::FtraceEventTable ftrace_event_table_{&string_pool_, &raw_table_};
+  tables::ChromeRawTable chrome_raw_table_{&string_pool_};
+  tables::FtraceEventTable ftrace_event_table_{&string_pool_};
 
   tables::MachineTable machine_table_{&string_pool_};
 
@@ -1097,9 +1062,6 @@
   tables::VulkanMemoryAllocationsTable vulkan_memory_allocations_table_{
       &string_pool_};
 
-  tables::GraphicsFrameSliceTable graphics_frame_slice_table_{&string_pool_,
-                                                              &slice_table_};
-
   // Metadata for memory snapshot.
   tables::MemorySnapshotTable memory_snapshot_table_{&string_pool_};
   tables::ProcessMemorySnapshotTable process_memory_snapshot_table_{
@@ -1107,12 +1069,6 @@
   tables::MemorySnapshotNodeTable memory_snapshot_node_table_{&string_pool_};
   tables::MemorySnapshotEdgeTable memory_snapshot_edge_table_{&string_pool_};
 
-  // FrameTimeline tables
-  tables::ExpectedFrameTimelineSliceTable expected_frame_timeline_slice_table_{
-      &string_pool_, &slice_table_};
-  tables::ActualFrameTimelineSliceTable actual_frame_timeline_slice_table_{
-      &string_pool_, &slice_table_};
-
   // AndroidNetworkPackets tables
   tables::AndroidNetworkPacketsTable android_network_packets_table_{
       &string_pool_, &slice_table_};
diff --git a/src/trace_processor/storage_minimal_smoke_test.cc b/src/trace_processor/storage_minimal_smoke_test.cc
index 2ff90c7..ff3e638 100644
--- a/src/trace_processor/storage_minimal_smoke_test.cc
+++ b/src/trace_processor/storage_minimal_smoke_test.cc
@@ -33,9 +33,9 @@
 
 class JsonStringOutputWriter : public json::OutputWriter {
  public:
-  util::Status AppendString(const std::string& string) override {
+  base::Status AppendString(const std::string& string) override {
     buffer += string;
-    return util::OkStatus();
+    return base::OkStatus();
   }
   std::string buffer;
 };
@@ -54,7 +54,7 @@
   auto f = fopen(base::GetTestDataPath("test/data/gpu_trace.pb").c_str(), "rb");
   std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
   auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
-  util::Status status = storage_->Parse(std::move(buf), rsize);
+  base::Status status = storage_->Parse(std::move(buf), rsize);
   ASSERT_TRUE(status.ok());
   ASSERT_OK(storage_->NotifyEndOfFile());
 
@@ -78,7 +78,7 @@
       fopen(base::GetTestDataPath("test/data/systrace.html").c_str(), "rb");
   std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
   auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
-  util::Status status = storage_->Parse(std::move(buf), rsize);
+  base::Status status = storage_->Parse(std::move(buf), rsize);
 
   ASSERT_FALSE(status.ok());
 }
@@ -88,7 +88,7 @@
   auto f = fopen("test/data/track_event_typed_args.pb", "rb");
   std::unique_ptr<uint8_t[]> buf(new uint8_t[MAX_SIZE]);
   auto rsize = fread(reinterpret_cast<char*>(buf.get()), 1, MAX_SIZE, f);
-  util::Status status = storage_->Parse(std::move(buf), rsize);
+  base::Status status = storage_->Parse(std::move(buf), rsize);
   ASSERT_TRUE(status.ok());
   ASSERT_OK(storage_->NotifyEndOfFile());
 
diff --git a/src/trace_processor/tables/BUILD.gn b/src/trace_processor/tables/BUILD.gn
index 29891e6..8fccef1 100644
--- a/src/trace_processor/tables/BUILD.gn
+++ b/src/trace_processor/tables/BUILD.gn
@@ -15,6 +15,30 @@
 import("../../../gn/perfetto_tp_tables.gni")
 import("../../../gn/test.gni")
 
+source_set("tables") {
+  sources = [ "table_destructors.cc" ]
+  deps = [
+    ":macros_internal",
+    "../../../gn:default_deps",
+  ]
+  public_deps = [ ":tables_python" ]
+}
+
+source_set("macros_internal") {
+  sources = [
+    "macros_internal.cc",
+    "macros_internal.h",
+  ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/ext/base",
+    "../../../include/perfetto/trace_processor",
+    "../containers",
+    "../db:minimal",
+    "../db/column",
+  ]
+}
+
 perfetto_tp_tables("tables_python") {
   sources = [
     "android_tables.py",
@@ -36,23 +60,6 @@
   generate_docs = true
 }
 
-source_set("tables") {
-  sources = [
-    "macros_internal.cc",
-    "macros_internal.h",
-    "table_destructors.cc",
-  ]
-  deps = [
-    "../../../gn:default_deps",
-    "../../../include/perfetto/ext/base",
-    "../../../include/perfetto/trace_processor",
-    "../containers",
-    "../db:minimal",
-    "../db/column",
-  ]
-  public_deps = [ ":tables_python" ]
-}
-
 perfetto_tp_tables("py_tables_unittest") {
   sources = [ "py_tables_unittest.py" ]
 }
diff --git a/src/trace_processor/tables/android_tables.py b/src/trace_processor/tables/android_tables.py
index 65eae80..ab37535 100644
--- a/src/trace_processor/tables/android_tables.py
+++ b/src/trace_processor/tables/android_tables.py
@@ -164,7 +164,7 @@
     columns=[
         C('event_id', CppUint32()),
         C('ts', CppInt64()),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -183,8 +183,10 @@
                 ColumnDoc(
                     doc='Details of the motion event parsed from the proto message.',
                     joinable='args.arg_set_id'),
-            'base64_proto': 'Raw proto message encoded in base64',
-            'base64_proto_id': 'String id for raw proto message',
+            'base64_proto':
+                'Raw proto message encoded in base64',
+            'base64_proto_id':
+                'String id for raw proto message',
         }))
 
 ANDROID_KEY_EVENTS_TABLE = Table(
@@ -194,7 +196,7 @@
     columns=[
         C('event_id', CppUint32()),
         C('ts', CppInt64()),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -213,8 +215,10 @@
                 ColumnDoc(
                     doc='Details of the key event parsed from the proto message.',
                     joinable='args.arg_set_id'),
-            'base64_proto': 'Raw proto message encoded in base64',
-            'base64_proto_id': 'String id for raw proto message',
+            'base64_proto':
+                'Raw proto message encoded in base64',
+            'base64_proto_id':
+                'String id for raw proto message',
         }))
 
 ANDROID_INPUT_EVENT_DISPATCH_TABLE = Table(
@@ -223,7 +227,7 @@
     sql_name='__intrinsic_android_input_event_dispatch',
     columns=[
         C('event_id', CppUint32()),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('vsync_id', CppInt64()),
         C('window_id', CppInt32()),
         C('base64_proto', CppString()),
@@ -252,8 +256,10 @@
                 ''',
             'window_id':
                 'The id of the window to which the event was dispatched.',
-            'base64_proto': 'Raw proto message encoded in base64',
-            'base64_proto_id': 'String id for raw proto message',
+            'base64_proto':
+                'Raw proto message encoded in base64',
+            'base64_proto_id':
+                'String id for raw proto message',
         }))
 
 # Keep this list sorted.
diff --git a/src/trace_processor/tables/counter_tables.py b/src/trace_processor/tables/counter_tables.py
index b749559..efbea0c 100644
--- a/src/trace_processor/tables/counter_tables.py
+++ b/src/trace_processor/tables/counter_tables.py
@@ -35,16 +35,7 @@
         C('track_id', CppTableId(TRACK_TABLE)),
         C('value', CppDouble()),
         C('arg_set_id', CppOptional(CppUint32())),
-    ],
-    tabledoc=TableDoc(
-        doc='''''',
-        group='Events',
-        columns={
-            'ts': '''''',
-            'track_id': '''''',
-            'value': '''''',
-            'arg_set_id': '''''',
-        }))
+    ])
 
 # Keep this list sorted.
 ALL_TABLES = [
diff --git a/src/trace_processor/tables/flow_tables.py b/src/trace_processor/tables/flow_tables.py
index 7ed9652..7c9b370 100644
--- a/src/trace_processor/tables/flow_tables.py
+++ b/src/trace_processor/tables/flow_tables.py
@@ -31,7 +31,7 @@
         C('slice_out', CppTableId(SLICE_TABLE)),
         C('slice_in', CppTableId(SLICE_TABLE)),
         C('trace_id', CppOptional(CppInt64())),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='''''',
diff --git a/src/trace_processor/tables/metadata_tables.py b/src/trace_processor/tables/metadata_tables.py
index 10da09b..459d90d 100644
--- a/src/trace_processor/tables/metadata_tables.py
+++ b/src/trace_processor/tables/metadata_tables.py
@@ -62,7 +62,7 @@
         C('uid', CppOptional(CppUint32())),
         C('android_appid', CppOptional(CppUint32())),
         C('cmdline', CppOptional(CppString())),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('machine_id', CppOptional(CppTableId(MACHINE_TABLE))),
     ],
     wrapping_sql_view=WrappingSqlView(view_name='process',),
@@ -231,26 +231,36 @@
                 '''Extra args associated with the CPU''',
         }))
 
-RAW_TABLE = Table(
+CHROME_RAW_TABLE = Table(
     python_module=__file__,
-    class_name='RawTable',
-    sql_name='__intrinsic_raw',
+    class_name='ChromeRawTable',
+    sql_name='__intrinsic_chrome_raw',
+    columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('name', CppString()),
+        C('utid', CppTableId(THREAD_TABLE)),
+        C('arg_set_id', CppUint32()),
+    ])
+
+FTRACE_EVENT_TABLE = Table(
+    python_module=__file__,
+    class_name='FtraceEventTable',
+    sql_name='__intrinsic_ftrace_event',
     columns=[
         C('ts', CppInt64(), flags=ColumnFlag.SORTED),
         C('name', CppString()),
         C('utid', CppTableId(THREAD_TABLE)),
         C('arg_set_id', CppUint32()),
         C('common_flags', CppUint32()),
-        C('ucpu', CppTableId(CPU_TABLE))
+        C('ucpu', CppTableId(CPU_TABLE)),
     ],
-    wrapping_sql_view=WrappingSqlView('track'),
+    wrapping_sql_view=WrappingSqlView('ftrace_event'),
     tabledoc=TableDoc(
         doc='''
-          Contains 'raw' events from the trace for some types of events. This
-          table only exists for debugging purposes and should not be relied on
-          in production usecases (i.e. metrics, standard library etc).
-
-          If you are looking for ftrace_events: please use the ftrace_event table.
+          Contains all the ftrace events in the trace. This table exists only
+          for debugging purposes and should not be relied on in production
+          usecases (i.e. metrics, standard library etc). Note also that this
+          table might be empty if raw ftrace parsing has been disabled.
         ''',
         group='Events',
         columns={
@@ -278,29 +288,14 @@
                 ''',
         }))
 
-FTRACE_EVENT_TABLE = Table(
-    python_module=__file__,
-    class_name='FtraceEventTable',
-    sql_name='__intrinsic_ftrace_event',
-    parent=RAW_TABLE,
-    columns=[],
-    wrapping_sql_view=WrappingSqlView('ftrace_event'),
-    tabledoc=TableDoc(
-        doc='''
-          Contains all the ftrace events in the trace. This table exists only
-          for debugging purposes and should not be relied on in production
-          usecases (i.e. metrics, standard library etc). Note also that this
-          table might be empty if raw ftrace parsing has been disabled.
-        ''',
-        group='Events',
-        columns={}))
-
 ARG_TABLE = Table(
     python_module=__file__,
     class_name='ArgTable',
     sql_name='__intrinsic_args',
     columns=[
-        C('arg_set_id', CppUint32(), flags=ColumnFlag.SORTED),
+        C('arg_set_id',
+          CppUint32(),
+          flags=ColumnFlag.SORTED | ColumnFlag.SET_ID),
         C('flat_key', CppString()),
         C('key', CppString()),
         C('int_value', CppOptional(CppInt64())),
@@ -489,6 +484,7 @@
 # Keep this list sorted.
 ALL_TABLES = [
     ARG_TABLE,
+    CHROME_RAW_TABLE,
     CLOCK_SNAPSHOT_TABLE,
     CPU_FREQ_TABLE,
     CPU_TABLE,
@@ -498,7 +494,6 @@
     MACHINE_TABLE,
     METADATA_TABLE,
     PROCESS_TABLE,
-    RAW_TABLE,
     THREAD_TABLE,
     TRACE_FILE_TABLE,
 ]
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index 67e1018..9fdf061 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -290,8 +290,8 @@
     columns=[
         C('ts', CppInt64(), flags=ColumnFlag.SORTED),
         C('utid', CppUint32()),
-        C('cpu', CppOptional(CppUint32())),
         C('callsite_id', CppOptional(CppTableId(STACK_PROFILE_CALLSITE_TABLE))),
+        C('cpu', CppOptional(CppUint32())),
     ],
     tabledoc=TableDoc(
         doc='''
@@ -303,10 +303,10 @@
                 '''Timestamp of the sample.''',
             'utid':
                 '''Sampled thread.''',
-            'cpu':
-                '''Core the sampled thread was running on.''',
             'callsite_id':
                 '''If set, unwound callstack of the sampled thread.''',
+            'cpu':
+                '''Core the sampled thread was running on.''',
         }))
 
 SYMBOL_TABLE = Table(
diff --git a/src/trace_processor/tables/py_tables_unittest.cc b/src/trace_processor/tables/py_tables_unittest.cc
index d13b65a..45415bf 100644
--- a/src/trace_processor/tables/py_tables_unittest.cc
+++ b/src/trace_processor/tables/py_tables_unittest.cc
@@ -15,6 +15,7 @@
  */
 
 #include <cstdint>
+#include <optional>
 #include <utility>
 #include <vector>
 
@@ -55,7 +56,7 @@
   ASSERT_EQ(TestEventTable::ColumnFlag::ts,
             ColumnLegacy::Flag::kSorted | ColumnLegacy::Flag::kNonNull);
   ASSERT_EQ(TestEventTable::ColumnFlag::arg_set_id,
-            ColumnLegacy::Flag::kNonNull);
+            ColumnLegacy::Flag::kNoFlag);
 }
 
 TEST_F(PyTablesUnittest, ArgsTableProprties) {
@@ -79,11 +80,11 @@
 TEST_F(PyTablesUnittest, InsertEventSpecifyCols) {
   TestEventTable::Row row;
   row.ts = 100;
-  row.arg_set_id = 0;
+  row.arg_set_id = std::nullopt;
   event_.Insert(row);
 
   ASSERT_EQ(event_[0].ts(), 100);
-  ASSERT_EQ(event_[0].arg_set_id(), 0u);
+  ASSERT_EQ(event_[0].arg_set_id(), std::nullopt);
 }
 
 TEST_F(PyTablesUnittest, MutableColumn) {
diff --git a/src/trace_processor/tables/py_tables_unittest.py b/src/trace_processor/tables/py_tables_unittest.py
index 8acc688..7b63198 100644
--- a/src/trace_processor/tables/py_tables_unittest.py
+++ b/src/trace_processor/tables/py_tables_unittest.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 """Contains tables for unittesting."""
 
-from python.generators.trace_processor_table.public import Column as C
+from python.generators.trace_processor_table.public import Column as C, CppOptional
 from python.generators.trace_processor_table.public import ColumnFlag
 from python.generators.trace_processor_table.public import CppInt64
 from python.generators.trace_processor_table.public import Table
@@ -25,7 +25,7 @@
     sql_name="event",
     columns=[
         C("ts", CppInt64(), flags=ColumnFlag.SORTED),
-        C("arg_set_id", CppUint32()),
+        C("arg_set_id", CppOptional(CppUint32())),
     ])
 
 EVENT_CHILD_TABLE = Table(
diff --git a/src/trace_processor/tables/slice_tables.py b/src/trace_processor/tables/slice_tables.py
index 99f64c8..e48402a 100644
--- a/src/trace_processor/tables/slice_tables.py
+++ b/src/trace_processor/tables/slice_tables.py
@@ -43,7 +43,7 @@
         C('stack_id', CppInt64()),
         C('parent_stack_id', CppInt64()),
         C('parent_id', CppOptional(CppSelfTableId())),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('thread_ts', CppOptional(CppInt64())),
         C('thread_dur', CppOptional(CppInt64())),
         C('thread_instruction_count', CppOptional(CppInt64())),
@@ -120,170 +120,6 @@
                 ''',
         }))
 
-GPU_SLICE_TABLE = Table(
-    python_module=__file__,
-    class_name='GpuSliceTable',
-    sql_name='gpu_slice',
-    columns=[
-        C('context_id', CppOptional(CppInt64())),
-        C('render_target', CppOptional(CppInt64())),
-        C('render_target_name', CppString()),
-        C('render_pass', CppOptional(CppInt64())),
-        C('render_pass_name', CppString()),
-        C('command_buffer', CppOptional(CppInt64())),
-        C('command_buffer_name', CppString()),
-        C('frame_id', CppOptional(CppUint32())),
-        C('submission_id', CppOptional(CppUint32())),
-        C('hw_queue_id', CppOptional(CppInt64())),
-        C('upid', CppOptional(CppUint32())),
-        C('render_subpasses', CppString()),
-    ],
-    parent=SLICE_TABLE,
-    tabledoc=TableDoc(
-        doc='''''',
-        group='Slice',
-        columns={
-            'context_id':
-                '''''',
-            'render_target':
-                '''''',
-            'render_target_name':
-                '''''',
-            'render_pass':
-                '''''',
-            'render_pass_name':
-                '''''',
-            'command_buffer':
-                '''''',
-            'command_buffer_name':
-                '''''',
-            'frame_id':
-                '''''',
-            'submission_id':
-                '''''',
-            'hw_queue_id':
-                '''''',
-            'upid':
-                '''
-                  Unique process id of the app that generates this gpu render
-                  stage event.
-                ''',
-            'render_subpasses':
-                ''''''
-        }))
-
-GRAPHICS_FRAME_SLICE_TABLE = Table(
-    python_module=__file__,
-    class_name='GraphicsFrameSliceTable',
-    sql_name='frame_slice',
-    columns=[
-        C('frame_number', CppUint32()),
-        C('layer_name', CppString()),
-        C('queue_to_acquire_time', CppInt64()),
-        C('acquire_to_latch_time', CppInt64()),
-        C('latch_to_present_time', CppInt64()),
-    ],
-    parent=SLICE_TABLE,
-    tabledoc=TableDoc(
-        doc='''''',
-        group='Slice',
-        columns={
-            'frame_number': '''''',
-            'layer_name': '''''',
-            'queue_to_acquire_time': '''''',
-            'acquire_to_latch_time': '''''',
-            'latch_to_present_time': ''''''
-        }))
-
-EXPECTED_FRAME_TIMELINE_SLICE_TABLE = Table(
-    python_module=__file__,
-    class_name='ExpectedFrameTimelineSliceTable',
-    sql_name='expected_frame_timeline_slice',
-    columns=[
-        C('display_frame_token', CppInt64()),
-        C('surface_frame_token', CppInt64()),
-        C('upid', CppUint32()),
-        C('layer_name', CppString()),
-    ],
-    parent=SLICE_TABLE,
-    tabledoc=TableDoc(
-        doc='''
-        This table contains information on the expected timeline of either
-        a display frame or a surface frame.
-        ''',
-        group='Slice',
-        columns={
-            'display_frame_token':
-                'Display frame token (vsync id).',
-            'surface_frame_token':
-                '''
-                Surface frame token (vsync id), null if this is a display frame.
-                ''',
-            'upid':
-                '''
-                Unique process id of the app that generates the surface frame.
-                ''',
-            'layer_name':
-                'Layer name if this is a surface frame.',
-        }))
-
-ACTUAL_FRAME_TIMELINE_SLICE_TABLE = Table(
-    python_module=__file__,
-    class_name='ActualFrameTimelineSliceTable',
-    sql_name='actual_frame_timeline_slice',
-    columns=[
-        C('display_frame_token', CppInt64()),
-        C('surface_frame_token', CppInt64()),
-        C('upid', CppUint32()),
-        C('layer_name', CppString()),
-        C('present_type', CppString()),
-        C('on_time_finish', CppInt32()),
-        C('gpu_composition', CppInt32()),
-        C('jank_type', CppString()),
-        C('jank_severity_type', CppString()),
-        C('prediction_type', CppString()),
-        C('jank_tag', CppString()),
-    ],
-    parent=SLICE_TABLE,
-    tabledoc=TableDoc(
-        doc='''
-        This table contains information on the actual timeline and additional
-        analysis related to the performance of either a display frame or a
-        surface frame.
-        ''',
-        group='Slice',
-        columns={
-            'display_frame_token':
-                'Display frame token (vsync id).',
-            'surface_frame_token':
-                '''
-                Surface frame token (vsync id), null if this is a display frame.
-                ''',
-            'upid':
-                '''
-                Unique process id of the app that generates the surface frame.
-                ''',
-            'layer_name':
-                'Layer name if this is a surface frame.',
-            'present_type':
-                'Frame\'s present type (eg. on time / early / late).',
-            'on_time_finish':
-                'Whether the frame finishes on time.',
-            'gpu_composition':
-                'Whether the frame used gpu composition.',
-            'jank_type':
-                '''
-                Specify the jank types for this frame if there's jank, or
-                none if no jank occurred.
-                ''',
-            'jank_severity_type':
-                'Severity of the jank: none if no jank.',
-            'prediction_type':
-                'Frame\'s prediction type (eg. valid / expired).',
-            'jank_tag':
-                'Jank tag based on jank type, used for slice visualization.'
-        }))
-
 EXPERIMENTAL_FLAT_SLICE_TABLE = Table(
     python_module=__file__,
     class_name='ExperimentalFlatSliceTable',
@@ -294,7 +130,7 @@
         C('track_id', CppTableId(TRACK_TABLE)),
         C('category', CppOptional(CppString())),
         C('name', CppOptional(CppString())),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('source_id', CppOptional(CppTableId(SLICE_TABLE))),
         C('start_bound', CppInt64(), flags=ColumnFlag.HIDDEN),
         C('end_bound', CppInt64(), flags=ColumnFlag.HIDDEN),
@@ -351,56 +187,11 @@
         C('packet_tcp_flags', CppOptional(CppUint32())),
         C('packet_tcp_flags_str', CppOptional(CppString())),
     ],
-    parent=SLICE_TABLE,
-    wrapping_sql_view=WrappingSqlView('android_network_packets'),
-    tabledoc=TableDoc(
-        doc="""
-        This table contains details on Android Network activity.
-        """,
-        group='Slice',
-        columns={
-            'iface':
-                'The name of the network interface used',
-            'direction':
-                'The direction of traffic (Received or Transmitted)',
-            'packet_transport':
-                'The transport protocol of packets in this event',
-            'packet_length':
-                'The length (in bytes) of packets in this event',
-            'packet_count':
-                'The number of packets contained in this event',
-            'socket_tag':
-                'The Android network tag of the socket',
-            'socket_tag_str':
-                'The socket tag formatted as a hex string',
-            'socket_uid':
-                'The Linux user id of the socket',
-            'local_port':
-                'The local udp/tcp port',
-            'remote_port':
-                'The remote udp/tcp port',
-            'packet_icmp_type':
-                'The 1-byte ICMP type identifier',
-            'packet_icmp_code':
-                'The 1-byte ICMP code identifier',
-            'packet_tcp_flags':
-                'The TCP flags as an integer bitmask (FIN=0x1, SYN=0x2, etc)',
-            'packet_tcp_flags_str':
-                '''
-                The TCP flags formatted as a string bitmask (e.g. "f...a..." for
-                FIN & ACK)
-                ''',
-        },
-    ),
-)
+    parent=SLICE_TABLE)
 
 # Keep this list sorted.
 ALL_TABLES = [
-    ACTUAL_FRAME_TIMELINE_SLICE_TABLE,
     ANDROID_NETWORK_PACKETS_TABLE,
-    EXPECTED_FRAME_TIMELINE_SLICE_TABLE,
     EXPERIMENTAL_FLAT_SLICE_TABLE,
-    GPU_SLICE_TABLE,
-    GRAPHICS_FRAME_SLICE_TABLE,
     SLICE_TABLE,
 ]
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index b538861..f3f7b20 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -61,7 +61,7 @@
 JitFrameTable::~JitFrameTable() = default;
 
 // metadata_tables_py.h
-RawTable::~RawTable() = default;
+ChromeRawTable::~ChromeRawTable() = default;
 FtraceEventTable::~FtraceEventTable() = default;
 ArgTable::~ArgTable() = default;
 ExpMissingChromeProcTable::~ExpMissingChromeProcTable() = default;
@@ -106,10 +106,6 @@
 // slice_tables_py.h
 SliceTable::~SliceTable() = default;
 FlowTable::~FlowTable() = default;
-GpuSliceTable::~GpuSliceTable() = default;
-GraphicsFrameSliceTable::~GraphicsFrameSliceTable() = default;
-ExpectedFrameTimelineSliceTable::~ExpectedFrameTimelineSliceTable() = default;
-ActualFrameTimelineSliceTable::~ActualFrameTimelineSliceTable() = default;
 ExperimentalFlatSliceTable::~ExperimentalFlatSliceTable() = default;
 AndroidNetworkPacketsTable::~AndroidNetworkPacketsTable() = default;
 
diff --git a/src/trace_processor/tables/winscope_tables.py b/src/trace_processor/tables/winscope_tables.py
index a218e6b..07dc4ee 100644
--- a/src/trace_processor/tables/winscope_tables.py
+++ b/src/trace_processor/tables/winscope_tables.py
@@ -29,7 +29,7 @@
     sql_name='__intrinsic_inputmethod_clients',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -49,7 +49,7 @@
     sql_name='__intrinsic_inputmethod_manager_service',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -69,7 +69,7 @@
     sql_name='__intrinsic_inputmethod_service',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -89,7 +89,7 @@
     sql_name='surfaceflinger_layers_snapshot',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -109,7 +109,7 @@
     sql_name='surfaceflinger_layer',
     columns=[
         C('snapshot_id', CppTableId(SURFACE_FLINGER_LAYERS_SNAPSHOT_TABLE)),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -129,7 +129,7 @@
     sql_name='surfaceflinger_transactions',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -150,7 +150,7 @@
     sql_name='__intrinsic_viewcapture',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -171,7 +171,7 @@
     columns=[
         C('ts', CppInt64()),
         C('transition_id', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
@@ -212,7 +212,7 @@
     sql_name='__intrinsic_windowmanager',
     columns=[
         C('ts', CppInt64(), ColumnFlag.SORTED),
-        C('arg_set_id', CppUint32()),
+        C('arg_set_id', CppOptional(CppUint32())),
         C('base64_proto', CppString()),
         C('base64_proto_id', CppOptional(CppUint32())),
     ],
diff --git a/src/trace_processor/trace_parsing_fuzzer.cc b/src/trace_processor/trace_parsing_fuzzer.cc
index 7e9a1e9..94cbadc 100644
--- a/src/trace_processor/trace_parsing_fuzzer.cc
+++ b/src/trace_processor/trace_parsing_fuzzer.cc
@@ -27,7 +27,7 @@
       TraceProcessorStorage::CreateInstance(Config());
   std::unique_ptr<uint8_t[]> buf(new uint8_t[size]);
   memcpy(buf.get(), data, size);
-  util::Status status = processor->Parse(std::move(buf), size);
+  base::Status status = processor->Parse(std::move(buf), size);
   if (!status.ok())
     return;
   if (auto s = processor->NotifyEndOfFile(); !s.ok()) {
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 6922757..d32658e 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -19,7 +19,6 @@
 #include <memory>
 #include <optional>
 
-#include "perfetto/base/logging.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/args_translation_table.h"
@@ -42,12 +41,9 @@
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/importers/common/track_compressor.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
-#include "src/trace_processor/importers/proto/android_track_event.descriptor.h"
-#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
-#include "src/trace_processor/importers/proto/track_event.descriptor.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_reader_registry.h"
 
@@ -57,48 +53,34 @@
     : config(args.config), storage(args.storage) {
   reader_registry = std::make_unique<TraceReaderRegistry>(this);
   // Init the trackers.
-  machine_tracker.reset(new MachineTracker(this, args.raw_machine_id));
+  machine_tracker = std::make_unique<MachineTracker>(this, args.raw_machine_id);
   if (!machine_id()) {
-    multi_machine_trace_manager.reset(new MultiMachineTraceManager(this));
+    multi_machine_trace_manager =
+        std::make_unique<MultiMachineTraceManager>(this);
   }
-  track_tracker.reset(new TrackTracker(this));
-  track_compressor.reset(new TrackCompressor(this));
-  args_tracker.reset(new ArgsTracker(this));
-  args_translation_table.reset(new ArgsTranslationTable(storage.get()));
-  slice_tracker.reset(new SliceTracker(this));
-  slice_translation_table.reset(new SliceTranslationTable(storage.get()));
-  flow_tracker.reset(new FlowTracker(this));
-  event_tracker.reset(new EventTracker(this));
-  sched_event_tracker.reset(new SchedEventTracker(this));
-  process_tracker.reset(new ProcessTracker(this));
-  process_track_translation_table.reset(
-      new ProcessTrackTranslationTable(storage.get()));
-  clock_tracker.reset(new ClockTracker(this));
-  clock_converter.reset(new ClockConverter(this));
-  mapping_tracker.reset(new MappingTracker(this));
-  perf_sample_tracker.reset(new PerfSampleTracker(this));
-  stack_profile_tracker.reset(new StackProfileTracker(this));
-  metadata_tracker.reset(new MetadataTracker(storage.get()));
-  cpu_tracker.reset(new CpuTracker(this));
-  global_args_tracker.reset(new GlobalArgsTracker(storage.get()));
-  {
-    descriptor_pool_.reset(new DescriptorPool());
-    auto status = descriptor_pool_->AddFromFileDescriptorSet(
-        kTrackEventDescriptor.data(), kTrackEventDescriptor.size());
-
-    PERFETTO_DCHECK(status.ok());
-
-    status = descriptor_pool_->AddFromFileDescriptorSet(
-        kChromeTrackEventDescriptor.data(), kChromeTrackEventDescriptor.size());
-
-    PERFETTO_DCHECK(status.ok());
-
-    status = descriptor_pool_->AddFromFileDescriptorSet(
-        kAndroidTrackEventDescriptor.data(),
-        kAndroidTrackEventDescriptor.size());
-
-    PERFETTO_DCHECK(status.ok());
-  }
+  track_tracker = std::make_unique<TrackTracker>(this);
+  track_compressor = std::make_unique<TrackCompressor>(this);
+  args_tracker = std::make_unique<ArgsTracker>(this);
+  args_translation_table =
+      std::make_unique<ArgsTranslationTable>(storage.get());
+  slice_tracker = std::make_unique<SliceTracker>(this);
+  slice_translation_table =
+      std::make_unique<SliceTranslationTable>(storage.get());
+  flow_tracker = std::make_unique<FlowTracker>(this);
+  event_tracker = std::make_unique<EventTracker>(this);
+  sched_event_tracker = std::make_unique<SchedEventTracker>(this);
+  process_tracker = std::make_unique<ProcessTracker>(this);
+  process_track_translation_table =
+      std::make_unique<ProcessTrackTranslationTable>(storage.get());
+  clock_tracker = std::make_unique<ClockTracker>(this);
+  clock_converter = std::make_unique<ClockConverter>(this);
+  mapping_tracker = std::make_unique<MappingTracker>(this);
+  perf_sample_tracker = std::make_unique<PerfSampleTracker>(this);
+  stack_profile_tracker = std::make_unique<StackProfileTracker>(this);
+  metadata_tracker = std::make_unique<MetadataTracker>(storage.get());
+  cpu_tracker = std::make_unique<CpuTracker>(this);
+  global_args_tracker = std::make_shared<GlobalArgsTracker>(storage.get());
+  descriptor_pool_ = std::make_unique<DescriptorPool>();
 
   slice_tracker->SetOnSliceBeginCallback(
       [this](TrackId track_id, SliceId slice_id) {
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 1061081..6b7f35c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -17,7 +17,6 @@
 #include "src/trace_processor/trace_processor_impl.h"
 
 #include <algorithm>
-#include <chrono>
 #include <cinttypes>
 #include <cstddef>
 #include <cstdint>
@@ -46,8 +45,8 @@
 #include "perfetto/trace_processor/iterator.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "perfetto/trace_processor/trace_processor.h"
-#include "src/trace_processor/importers/android_bugreport/android_dumpstate_reader.h"
 #include "src/trace_processor/importers/android_bugreport/android_dumpstate_event_parser_impl.h"
+#include "src/trace_processor/importers/android_bugreport/android_dumpstate_reader.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event_parser_impl.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
 #include "src/trace_processor/importers/archive/gzip_trace_parser.h"
@@ -67,6 +66,7 @@
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/ninja/ninja_log_parser.h"
 #include "src/trace_processor/importers/perf/perf_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/importers/perf/perf_tracker.h"
 #include "src/trace_processor/importers/perf/record_parser.h"
 #include "src/trace_processor/importers/perf/spe_record_parser.h"
@@ -112,10 +112,8 @@
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/descendant.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/dfs_weight_bounded.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_annotated_stack.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_counter_dur.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flat_slice.h"
-#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_sched_upid.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_slice_layout.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/table_info.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/winscope_proto_to_args_with_defaults.h"
@@ -139,6 +137,11 @@
 #include "src/trace_processor/util/status_macros.h"
 #include "src/trace_processor/util/trace_type.h"
 
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
 #include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
 #include "src/trace_processor/importers/instruments/row_parser.h"
@@ -149,14 +152,9 @@
 #include "src/trace_processor/importers/etm/etm_v4_stream_demultiplexer.h"
 #include "src/trace_processor/importers/etm/file_tracker.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/operators/etm_decode_trace_vtable.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/etm_iterate_range_vtable.h"
 #endif
 
-#include "protos/perfetto/common/builtin_clock.pbzero.h"
-#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
-#include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
-#include "protos/perfetto/trace/trace.pbzero.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-
 namespace perfetto::trace_processor {
 namespace {
 
@@ -347,7 +345,7 @@
     const TraceStorage& storage) {
   int64_t start_ns = std::numeric_limits<int64_t>::max();
   int64_t end_ns = std::numeric_limits<int64_t>::min();
-  for (auto it = storage.raw_table().IterateRows(); it; ++it) {
+  for (auto it = storage.ftrace_event_table().IterateRows(); it; ++it) {
     start_ns = std::min(it.ts(), start_ns);
     end_ns = std::max(it.ts(), end_ns);
   }
@@ -476,17 +474,11 @@
 
   context_.reader_registry->RegisterTraceReader<TarTraceReader>(kTarTraceType);
 
-  if (context_.config.analyze_trace_proto_content) {
-    context_.content_analyzer =
-        std::make_unique<ProtoContentAnalyzer>(&context_);
-  }
-
 #if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
   perf_importer::PerfTracker::GetOrCreate(&context_)->RegisterAuxTokenizer(
       PERF_AUXTRACE_CS_ETM, etm::CreateEtmV4StreamDemultiplexer);
 #endif
 
-  // Add metrics to descriptor pool
   const std::vector<std::string> sanitized_extension_paths =
       SanitizeMetricMountPaths(config_.skip_builtin_metric_paths);
   std::vector<std::string> skip_prefixes;
@@ -494,14 +486,16 @@
   for (const auto& path : sanitized_extension_paths) {
     skip_prefixes.push_back(kMetricProtoRoot + path);
   }
-  pool_.AddFromFileDescriptorSet(kMetricsDescriptor.data(),
-                                 kMetricsDescriptor.size(), skip_prefixes);
-  pool_.AddFromFileDescriptorSet(kAllChromeMetricsDescriptor.data(),
-                                 kAllChromeMetricsDescriptor.size(),
-                                 skip_prefixes);
-  pool_.AddFromFileDescriptorSet(kAllWebviewMetricsDescriptor.data(),
-                                 kAllWebviewMetricsDescriptor.size(),
-                                 skip_prefixes);
+
+  // Add metrics to descriptor pool
+  metrics_descriptor_pool_.AddFromFileDescriptorSet(
+      kMetricsDescriptor.data(), kMetricsDescriptor.size(), skip_prefixes);
+  metrics_descriptor_pool_.AddFromFileDescriptorSet(
+      kAllChromeMetricsDescriptor.data(), kAllChromeMetricsDescriptor.size(),
+      skip_prefixes);
+  metrics_descriptor_pool_.AddFromFileDescriptorSet(
+      kAllWebviewMetricsDescriptor.data(), kAllWebviewMetricsDescriptor.size(),
+      skip_prefixes);
 
   RegisterAdditionalModules(&context_);
   InitPerfettoSqlEngine();
@@ -522,22 +516,15 @@
 
 TraceProcessorImpl::~TraceProcessorImpl() = default;
 
+// =================================================================
+// |        TraceProcessorStorage implementation starts here       |
+// =================================================================
+
 base::Status TraceProcessorImpl::Parse(TraceBlobView blob) {
   bytes_parsed_ += blob.size();
   return TraceProcessorStorageImpl::Parse(std::move(blob));
 }
 
-std::string TraceProcessorImpl::GetCurrentTraceName() {
-  if (current_trace_name_.empty())
-    return "";
-  auto size = " (" + std::to_string(bytes_parsed_ / 1024 / 1024) + " MB)";
-  return current_trace_name_ + size;
-}
-
-void TraceProcessorImpl::SetCurrentTraceName(const std::string& name) {
-  current_trace_name_ = name;
-}
-
 void TraceProcessorImpl::Flush() {
   TraceProcessorStorageImpl::Flush();
   BuildBoundsTable(engine_->sqlite_engine()->db(),
@@ -587,19 +574,9 @@
   return base::OkStatus();
 }
 
-size_t TraceProcessorImpl::RestoreInitialTables() {
-  // We should always have at least as many objects now as we did in the
-  // constructor.
-  uint64_t registered_count_before = engine_->SqliteRegisteredObjectCount();
-  PERFETTO_CHECK(registered_count_before >= sqlite_objects_post_prelude_);
-
-  InitPerfettoSqlEngine();
-
-  // The registered count should now be the same as it was in the constructor.
-  uint64_t registered_count_after = engine_->SqliteRegisteredObjectCount();
-  PERFETTO_CHECK(registered_count_after == sqlite_objects_post_prelude_);
-  return static_cast<size_t>(registered_count_before - registered_count_after);
-}
+// =================================================================
+// |        PerfettoSQL related functionality starts here          |
+// =================================================================
 
 Iterator TraceProcessorImpl::ExecuteQuery(const std::string& sql) {
   PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "EXECUTE_QUERY",
@@ -617,23 +594,6 @@
   return Iterator(std::move(impl));
 }
 
-void TraceProcessorImpl::InterruptQuery() {
-  if (!engine_->sqlite_engine()->db())
-    return;
-  query_interrupted_.store(true);
-  sqlite3_interrupt(engine_->sqlite_engine()->db());
-}
-
-bool TraceProcessorImpl::IsRootMetricField(const std::string& metric_name) {
-  std::optional<uint32_t> desc_idx =
-      pool_.FindDescriptorIdx(".perfetto.protos.TraceMetrics");
-  if (!desc_idx.has_value())
-    return false;
-  const auto* field_idx =
-      pool_.descriptors()[*desc_idx].FindFieldByName(metric_name);
-  return field_idx != nullptr;
-}
-
 base::Status TraceProcessorImpl::RegisterSqlPackage(SqlPackage sql_package) {
   sql_modules::RegisteredPackage new_package;
   std::string name = sql_package.name;
@@ -661,6 +621,151 @@
   return base::OkStatus();
 }
 
+base::Status TraceProcessorImpl::RegisterSqlModule(SqlModule module) {
+  SqlPackage package;
+  package.name = std::move(module.name);
+  package.modules = std::move(module.files);
+  package.allow_override = module.allow_module_override;
+  return RegisterSqlPackage(package);
+}
+
+// =================================================================
+// |        Metatracing related functionality starts here          |
+// =================================================================
+
+void TraceProcessorImpl::EnableMetatrace(MetatraceConfig config) {
+  metatrace::Enable(config);
+}
+
+namespace {
+
+class StringInterner {
+ public:
+  StringInterner(protos::pbzero::PerfettoMetatrace& event,
+                 base::FlatHashMap<std::string, uint64_t>& interned_strings)
+      : event_(event), interned_strings_(interned_strings) {}
+
+  ~StringInterner() {
+    for (const auto& interned_string : new_interned_strings_) {
+      auto* interned_string_proto = event_.add_interned_strings();
+      interned_string_proto->set_iid(interned_string.first);
+      interned_string_proto->set_value(interned_string.second);
+    }
+  }
+
+  uint64_t InternString(const std::string& str) {
+    uint64_t new_iid = interned_strings_.size();
+    auto insert_result = interned_strings_.Insert(str, new_iid);
+    if (insert_result.second) {
+      new_interned_strings_.emplace_back(new_iid, str);
+    }
+    return *insert_result.first;
+  }
+
+ private:
+  protos::pbzero::PerfettoMetatrace& event_;
+  base::FlatHashMap<std::string, uint64_t>& interned_strings_;
+
+  base::SmallVector<std::pair<uint64_t, std::string>, 16> new_interned_strings_;
+};
+
+}  // namespace
+
+base::Status TraceProcessorImpl::DisableAndReadMetatrace(
+    std::vector<uint8_t>* trace_proto) {
+  protozero::HeapBuffered<protos::pbzero::Trace> trace;
+
+  auto* clock_snapshot = trace->add_packet()->set_clock_snapshot();
+  for (const auto& [clock_id, ts] : base::CaptureClockSnapshots()) {
+    auto* clock = clock_snapshot->add_clocks();
+    clock->set_clock_id(clock_id);
+    clock->set_timestamp(ts);
+  }
+
+  auto tid = static_cast<uint32_t>(base::GetThreadId());
+  base::FlatHashMap<std::string, uint64_t> interned_strings;
+  metatrace::DisableAndReadBuffer(
+      [&trace, &interned_strings, tid](metatrace::Record* record) {
+        auto* packet = trace->add_packet();
+        packet->set_timestamp(record->timestamp_ns);
+        auto* evt = packet->set_perfetto_metatrace();
+
+        StringInterner interner(*evt, interned_strings);
+
+        evt->set_event_name_iid(interner.InternString(record->event_name));
+        evt->set_event_duration_ns(record->duration_ns);
+        evt->set_thread_id(tid);
+
+        if (record->args_buffer_size == 0)
+          return;
+
+        base::StringSplitter s(
+            record->args_buffer, record->args_buffer_size, '\0',
+            base::StringSplitter::EmptyTokenMode::ALLOW_EMPTY_TOKENS);
+        for (; s.Next();) {
+          auto* arg_proto = evt->add_args();
+          arg_proto->set_key_iid(interner.InternString(s.cur_token()));
+
+          bool has_next = s.Next();
+          PERFETTO_CHECK(has_next);
+          arg_proto->set_value_iid(interner.InternString(s.cur_token()));
+        }
+      });
+  *trace_proto = trace.SerializeAsArray();
+  return base::OkStatus();
+}
+
+// =================================================================
+// |              Advanced functionality starts here               |
+// =================================================================
+
+std::string TraceProcessorImpl::GetCurrentTraceName() {
+  if (current_trace_name_.empty())
+    return "";
+  auto size = " (" + std::to_string(bytes_parsed_ / 1024 / 1024) + " MB)";
+  return current_trace_name_ + size;
+}
+
+void TraceProcessorImpl::SetCurrentTraceName(const std::string& name) {
+  current_trace_name_ = name;
+}
+
+base::Status TraceProcessorImpl::RegisterFileContent(
+    [[maybe_unused]] const std::string& path,
+    [[maybe_unused]] TraceBlobView content) {
+#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
+  return etm::FileTracker::GetOrCreate(&context_)->AddFile(path,
+                                                           std::move(content));
+#else
+  return base::OkStatus();
+#endif
+}
+
+void TraceProcessorImpl::InterruptQuery() {
+  if (!engine_->sqlite_engine()->db())
+    return;
+  query_interrupted_.store(true);
+  sqlite3_interrupt(engine_->sqlite_engine()->db());
+}
+
+size_t TraceProcessorImpl::RestoreInitialTables() {
+  // We should always have at least as many objects now as we did in the
+  // constructor.
+  uint64_t registered_count_before = engine_->SqliteRegisteredObjectCount();
+  PERFETTO_CHECK(registered_count_before >= sqlite_objects_post_prelude_);
+
+  InitPerfettoSqlEngine();
+
+  // The registered count should now be the same as it was in the constructor.
+  uint64_t registered_count_after = engine_->SqliteRegisteredObjectCount();
+  PERFETTO_CHECK(registered_count_after == sqlite_objects_post_prelude_);
+  return static_cast<size_t>(registered_count_before - registered_count_after);
+}
+
+// =================================================================
+// |  Trace-based metrics (v1) related functionality starts here   |
+// =================================================================
+
 base::Status TraceProcessorImpl::RegisterMetric(const std::string& path,
                                                 const std::string& sql) {
   // Check if the metric with the given path already exists and if it does,
@@ -726,22 +831,26 @@
     const uint8_t* data,
     size_t size,
     const std::vector<std::string>& skip_prefixes) {
-  RETURN_IF_ERROR(pool_.AddFromFileDescriptorSet(data, size, skip_prefixes));
+  RETURN_IF_ERROR(metrics_descriptor_pool_.AddFromFileDescriptorSet(
+      data, size, skip_prefixes));
   RETURN_IF_ERROR(RegisterAllProtoBuilderFunctions(
-      &pool_, &proto_fn_name_to_path_, engine_.get(), this));
+      &metrics_descriptor_pool_, &proto_fn_name_to_path_, engine_.get(), this));
   return base::OkStatus();
 }
 
 base::Status TraceProcessorImpl::ComputeMetric(
     const std::vector<std::string>& metric_names,
     std::vector<uint8_t>* metrics_proto) {
-  auto opt_idx = pool_.FindDescriptorIdx(".perfetto.protos.TraceMetrics");
+  auto opt_idx = metrics_descriptor_pool_.FindDescriptorIdx(
+      ".perfetto.protos.TraceMetrics");
   if (!opt_idx.has_value())
     return base::Status("Root metrics proto descriptor not found");
 
-  const auto& root_descriptor = pool_.descriptors()[opt_idx.value()];
+  const auto& root_descriptor =
+      metrics_descriptor_pool_.descriptors()[opt_idx.value()];
   return metrics::ComputeMetrics(engine_.get(), metric_names, sql_metrics_,
-                                 pool_, root_descriptor, metrics_proto);
+                                 metrics_descriptor_pool_, root_descriptor,
+                                 metrics_proto);
 }
 
 base::Status TraceProcessorImpl::ComputeMetricText(
@@ -755,13 +864,13 @@
   switch (format) {
     case TraceProcessor::MetricResultFormat::kProtoText:
       *metrics_string = protozero_to_text::ProtozeroToText(
-          pool_, ".perfetto.protos.TraceMetrics",
+          metrics_descriptor_pool_, ".perfetto.protos.TraceMetrics",
           protozero::ConstBytes{metrics_proto.data(), metrics_proto.size()},
           protozero_to_text::kIncludeNewLines);
       break;
     case TraceProcessor::MetricResultFormat::kJson:
       *metrics_string = protozero_to_json::ProtozeroToJson(
-          pool_, ".perfetto.protos.TraceMetrics",
+          metrics_descriptor_pool_, ".perfetto.protos.TraceMetrics",
           protozero::ConstBytes{metrics_proto.data(), metrics_proto.size()},
           protozero_to_json::kPretty | protozero_to_json::kInlineErrors |
               protozero_to_json::kInlineAnnotations);
@@ -771,16 +880,12 @@
 }
 
 std::vector<uint8_t> TraceProcessorImpl::GetMetricDescriptors() {
-  return pool_.SerializeAsDescriptorSet();
-}
-
-void TraceProcessorImpl::EnableMetatrace(MetatraceConfig config) {
-  metatrace::Enable(config);
+  return metrics_descriptor_pool_.SerializeAsDescriptorSet();
 }
 
 void TraceProcessorImpl::InitPerfettoSqlEngine() {
-  engine_.reset(new PerfettoSqlEngine(context_.storage->mutable_string_pool(),
-                                      config_.enable_extra_checks));
+  engine_ = std::make_unique<PerfettoSqlEngine>(
+      context_.storage->mutable_string_pool(), config_.enable_extra_checks);
   sqlite3* db = engine_->sqlite_engine()->db();
   sqlite3_str_split_init(db);
 
@@ -908,6 +1013,9 @@
   engine_->sqlite_engine()
       ->RegisterVirtualTableModule<etm::EtmDecodeTraceVtable>(
           "__intrinsic_etm_decode_trace", storage);
+  engine_->sqlite_engine()
+      ->RegisterVirtualTableModule<etm::EtmIterateRangeVtable>(
+          "__intrinsic_etm_iterate_instruction_range", storage);
 #endif
 
   // Register stdlib packages.
@@ -951,7 +1059,7 @@
   // that table in TraceStorage::ShrinkToFitTables.
   RegisterStaticTable(storage->mutable_machine_table());
   RegisterStaticTable(storage->mutable_arg_table());
-  RegisterStaticTable(storage->mutable_raw_table());
+  RegisterStaticTable(storage->mutable_chrome_raw_table());
   RegisterStaticTable(storage->mutable_ftrace_event_table());
   RegisterStaticTable(storage->mutable_thread_table());
   RegisterStaticTable(storage->mutable_process_table());
@@ -963,7 +1071,6 @@
   RegisterStaticTable(storage->mutable_sched_slice_table());
   RegisterStaticTable(storage->mutable_spurious_sched_wakeup_table());
   RegisterStaticTable(storage->mutable_thread_state_table());
-  RegisterStaticTable(storage->mutable_gpu_slice_table());
 
   RegisterStaticTable(storage->mutable_track_table());
 
@@ -996,11 +1103,6 @@
 
   RegisterStaticTable(storage->mutable_vulkan_memory_allocations_table());
 
-  RegisterStaticTable(storage->mutable_graphics_frame_slice_table());
-
-  RegisterStaticTable(storage->mutable_expected_frame_timeline_slice_table());
-  RegisterStaticTable(storage->mutable_actual_frame_timeline_slice_table());
-
   RegisterStaticTable(storage->mutable_android_network_packets_table());
 
   RegisterStaticTable(storage->mutable_v8_isolate_table());
@@ -1063,8 +1165,6 @@
   engine_->RegisterStaticTableFunction(
       std::make_unique<ExperimentalFlamegraph>(&context_));
   engine_->RegisterStaticTableFunction(
-      std::make_unique<ExperimentalCounterDur>(storage->counter_table()));
-  engine_->RegisterStaticTableFunction(
       std::make_unique<ExperimentalSliceLayout>(
           context_.storage->mutable_string_pool(), &storage->slice_table()));
   engine_->RegisterStaticTableFunction(std::make_unique<TableInfo>(
@@ -1085,8 +1185,6 @@
       ConnectedFlow::Mode::kPrecedingFlow, context_.storage.get()));
   engine_->RegisterStaticTableFunction(std::make_unique<ConnectedFlow>(
       ConnectedFlow::Mode::kFollowingFlow, context_.storage.get()));
-  engine_->RegisterStaticTableFunction(std::make_unique<ExperimentalSchedUpid>(
-      storage->sched_slice_table(), storage->thread_table()));
   engine_->RegisterStaticTableFunction(
       std::make_unique<ExperimentalAnnotatedStack>(&context_));
   engine_->RegisterStaticTableFunction(
@@ -1105,8 +1203,9 @@
 
   // Metrics.
   {
-    auto status = RegisterAllProtoBuilderFunctions(
-        &pool_, &proto_fn_name_to_path_, engine_.get(), this);
+    auto status = RegisterAllProtoBuilderFunctions(&metrics_descriptor_pool_,
+                                                   &proto_fn_name_to_path_,
+                                                   engine_.get(), this);
     if (!status.ok()) {
       PERFETTO_FATAL("%s", status.c_message());
     }
@@ -1149,93 +1248,15 @@
   }
 }
 
-namespace {
-
-class StringInterner {
- public:
-  StringInterner(protos::pbzero::PerfettoMetatrace& event,
-                 base::FlatHashMap<std::string, uint64_t>& interned_strings)
-      : event_(event), interned_strings_(interned_strings) {}
-
-  ~StringInterner() {
-    for (const auto& interned_string : new_interned_strings_) {
-      auto* interned_string_proto = event_.add_interned_strings();
-      interned_string_proto->set_iid(interned_string.first);
-      interned_string_proto->set_value(interned_string.second);
-    }
-  }
-
-  uint64_t InternString(const std::string& str) {
-    uint64_t new_iid = interned_strings_.size();
-    auto insert_result = interned_strings_.Insert(str, new_iid);
-    if (insert_result.second) {
-      new_interned_strings_.emplace_back(new_iid, str);
-    }
-    return *insert_result.first;
-  }
-
- private:
-  protos::pbzero::PerfettoMetatrace& event_;
-  base::FlatHashMap<std::string, uint64_t>& interned_strings_;
-
-  base::SmallVector<std::pair<uint64_t, std::string>, 16> new_interned_strings_;
-};
-
-}  // namespace
-
-base::Status TraceProcessorImpl::DisableAndReadMetatrace(
-    std::vector<uint8_t>* trace_proto) {
-  protozero::HeapBuffered<protos::pbzero::Trace> trace;
-
-  auto* clock_snapshot = trace->add_packet()->set_clock_snapshot();
-  for (const auto& [clock_id, ts] : base::CaptureClockSnapshots()) {
-    auto* clock = clock_snapshot->add_clocks();
-    clock->set_clock_id(clock_id);
-    clock->set_timestamp(ts);
-  }
-
-  auto tid = static_cast<uint32_t>(base::GetThreadId());
-  base::FlatHashMap<std::string, uint64_t> interned_strings;
-  metatrace::DisableAndReadBuffer(
-      [&trace, &interned_strings, tid](metatrace::Record* record) {
-        auto* packet = trace->add_packet();
-        packet->set_timestamp(record->timestamp_ns);
-        auto* evt = packet->set_perfetto_metatrace();
-
-        StringInterner interner(*evt, interned_strings);
-
-        evt->set_event_name_iid(interner.InternString(record->event_name));
-        evt->set_event_duration_ns(record->duration_ns);
-        evt->set_thread_id(tid);
-
-        if (record->args_buffer_size == 0)
-          return;
-
-        base::StringSplitter s(
-            record->args_buffer, record->args_buffer_size, '\0',
-            base::StringSplitter::EmptyTokenMode::ALLOW_EMPTY_TOKENS);
-        for (; s.Next();) {
-          auto* arg_proto = evt->add_args();
-          arg_proto->set_key_iid(interner.InternString(s.cur_token()));
-
-          bool has_next = s.Next();
-          PERFETTO_CHECK(has_next);
-          arg_proto->set_value_iid(interner.InternString(s.cur_token()));
-        }
-      });
-  *trace_proto = trace.SerializeAsArray();
-  return base::OkStatus();
-}
-
-base::Status TraceProcessorImpl::RegisterFileContent(
-    [[maybe_unused]] const std::string& path,
-    [[maybe_unused]] TraceBlobView content) {
-#if PERFETTO_BUILDFLAG(PERFETTO_ENABLE_ETM_IMPORTER)
-  return etm::FileTracker::GetOrCreate(&context_)->AddFile(path,
-                                                           std::move(content));
-#else
-  return base::OkStatus();
-#endif
+bool TraceProcessorImpl::IsRootMetricField(const std::string& metric_name) {
+  std::optional<uint32_t> desc_idx = metrics_descriptor_pool_.FindDescriptorIdx(
+      ".perfetto.protos.TraceMetrics");
+  if (!desc_idx.has_value())
+    return false;
+  const auto* field_idx =
+      metrics_descriptor_pool_.descriptors()[*desc_idx].FindFieldByName(
+          metric_name);
+  return field_idx != nullptr;
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index d2a716a..0706cd3 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -25,6 +25,7 @@
 #include <memory>
 #include <string>
 #include <unordered_map>
+#include <utility>
 #include <vector>
 
 #include "perfetto/base/status.h"
@@ -56,21 +57,55 @@
 
   ~TraceProcessorImpl() override;
 
-  // TraceProcessorStorage implementation:
+  // =================================================================
+  // |        TraceProcessorStorage implementation starts here       |
+  // =================================================================
+
   base::Status Parse(TraceBlobView) override;
   void Flush() override;
   base::Status NotifyEndOfFile() override;
 
-  // TraceProcessor implementation:
+  // =================================================================
+  // |        PerfettoSQL related functionality starts here          |
+  // =================================================================
+
   Iterator ExecuteQuery(const std::string& sql) override;
 
+  base::Status RegisterSqlPackage(SqlPackage) override;
+
+  base::Status RegisterSqlModule(SqlModule module) override;
+
+  // =================================================================
+  // |        Metatracing related functionality starts here          |
+  // =================================================================
+
+  void EnableMetatrace(MetatraceConfig config) override;
+
+  base::Status DisableAndReadMetatrace(
+      std::vector<uint8_t>* trace_proto) override;
+
+  // =================================================================
+  // |              Advanced functionality starts here               |
+  // =================================================================
+
+  std::string GetCurrentTraceName() override;
+  void SetCurrentTraceName(const std::string&) override;
+
+  base::Status RegisterFileContent(const std::string& path,
+                                   TraceBlobView content) override;
+
+  void InterruptQuery() override;
+
+  size_t RestoreInitialTables() override;
+
+  // =================================================================
+  // |  Trace-based metrics (v1) related functionality starts here   |
+  // =================================================================
+
   base::Status RegisterMetric(const std::string& path,
                               const std::string& sql) override;
 
-  base::Status RegisterSqlPackage(SqlPackage) override;
-
   base::Status ExtendMetricsProto(const uint8_t* data, size_t size) override;
-
   base::Status ExtendMetricsProto(
       const uint8_t* data,
       size_t size,
@@ -78,37 +113,12 @@
 
   base::Status ComputeMetric(const std::vector<std::string>& metric_names,
                              std::vector<uint8_t>* metrics) override;
-
   base::Status ComputeMetricText(const std::vector<std::string>& metric_names,
                                  TraceProcessor::MetricResultFormat format,
                                  std::string* metrics_string) override;
 
   std::vector<uint8_t> GetMetricDescriptors() override;
 
-  void InterruptQuery() override;
-
-  size_t RestoreInitialTables() override;
-
-  std::string GetCurrentTraceName() override;
-  void SetCurrentTraceName(const std::string&) override;
-
-  void EnableMetatrace(MetatraceConfig config) override;
-
-  base::Status DisableAndReadMetatrace(
-      std::vector<uint8_t>* trace_proto) override;
-
-  base::Status RegisterSqlModule(SqlModule module) override {
-    SqlPackage package;
-    package.name = std::move(module.name);
-    package.modules = std::move(module.files);
-    package.allow_override = module.allow_module_override;
-
-    return RegisterSqlPackage(package);
-  }
-
-  base::Status RegisterFileContent(const std::string& path,
-                                   TraceBlobView content) override;
-
  private:
   // Needed for iterators to be able to access the context.
   friend class IteratorImpl;
@@ -128,7 +138,7 @@
   const Config config_;
   std::unique_ptr<PerfettoSqlEngine> engine_;
 
-  DescriptorPool pool_;
+  DescriptorPool metrics_descriptor_pool_;
 
   std::vector<metrics::SqlMetricFile> sql_metrics_;
 
diff --git a/src/trace_processor/trace_processor_storage.cc b/src/trace_processor/trace_processor_storage.cc
index 36e6579..5a5fc1e 100644
--- a/src/trace_processor/trace_processor_storage.cc
+++ b/src/trace_processor/trace_processor_storage.cc
@@ -31,7 +31,7 @@
 
 TraceProcessorStorage::~TraceProcessorStorage() = default;
 
-util::Status TraceProcessorStorage::Parse(std::unique_ptr<uint8_t[]> buf,
+base::Status TraceProcessorStorage::Parse(std::unique_ptr<uint8_t[]> buf,
                                           size_t size) {
   return Parse(TraceBlobView(TraceBlob::TakeOwnership(std::move(buf), size)));
 }
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index da5540f..a3781f1 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -20,10 +20,8 @@
 #include <cstddef>
 #include <cstdint>
 #include <memory>
-#include <optional>
 #include <utility>
 
-#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/uuid.h"
@@ -38,10 +36,8 @@
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
-#include "src/trace_processor/importers/common/track_compressor.h"
 #include "src/trace_processor/importers/proto/default_modules.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
-#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
diff --git a/src/trace_processor/trace_processor_storage_impl.h b/src/trace_processor/trace_processor_storage_impl.h
index 9198d2f..a40493d 100644
--- a/src/trace_processor/trace_processor_storage_impl.h
+++ b/src/trace_processor/trace_processor_storage_impl.h
@@ -19,7 +19,6 @@
 
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
 #include "perfetto/trace_processor/trace_processor_storage.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -34,7 +33,7 @@
   explicit TraceProcessorStorageImpl(const Config&);
   ~TraceProcessorStorageImpl() override;
 
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   void Flush() override;
   base::Status NotifyEndOfFile() override;
 
diff --git a/src/trace_processor/util/debug_annotation_parser.cc b/src/trace_processor/util/debug_annotation_parser.cc
index 6669b4d..23a2820 100644
--- a/src/trace_processor/util/debug_annotation_parser.cc
+++ b/src/trace_processor/util/debug_annotation_parser.cc
@@ -94,8 +94,7 @@
     }
     delegate.AddString(context_name, decoder->str().ToStdString());
   } else if (annotation.has_pointer_value()) {
-    delegate.AddPointer(context_name, reinterpret_cast<const void*>(
-                                          annotation.pointer_value()));
+    delegate.AddPointer(context_name, annotation.pointer_value());
   } else if (annotation.has_dict_entries()) {
     bool added_entry = false;
     for (auto it = annotation.dict_entries(); it; ++it) {
diff --git a/src/trace_processor/util/debug_annotation_parser_unittest.cc b/src/trace_processor/util/debug_annotation_parser_unittest.cc
index 902e9da..c948a9d 100644
--- a/src/trace_processor/util/debug_annotation_parser_unittest.cc
+++ b/src/trace_processor/util/debug_annotation_parser_unittest.cc
@@ -97,10 +97,10 @@
     args_.push_back(ss.str());
   }
 
-  void AddPointer(const Key& key, const void* value) override {
+  void AddPointer(const Key& key, uint64_t value) override {
     std::stringstream ss;
-    ss << key.flat_key << " " << key.key << " " << std::hex
-       << reinterpret_cast<uintptr_t>(value) << std::dec;
+    ss << key.flat_key << " " << key.key << " " << std::hex << value
+       << std::dec;
     args_.push_back(ss.str());
   }
 
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 427ed41..d16be7f 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -85,7 +85,7 @@
                            const protozero::ConstChars& value) = 0;
     virtual void AddString(const Key& key, const std::string& value) = 0;
     virtual void AddDouble(const Key& key, double value) = 0;
-    virtual void AddPointer(const Key& key, const void* value) = 0;
+    virtual void AddPointer(const Key& key, uint64_t value) = 0;
     virtual void AddBoolean(const Key& key, bool value) = 0;
     virtual void AddBytes(const Key& key, const protozero::ConstBytes& value) {
       // In the absence of a better implementation default to showing
diff --git a/src/trace_processor/util/proto_to_args_parser_unittest.cc b/src/trace_processor/util/proto_to_args_parser_unittest.cc
index 3dae057..ace2f64 100644
--- a/src/trace_processor/util/proto_to_args_parser_unittest.cc
+++ b/src/trace_processor/util/proto_to_args_parser_unittest.cc
@@ -104,10 +104,10 @@
     args_.push_back(ss.str());
   }
 
-  void AddPointer(const Key& key, const void* value) override {
+  void AddPointer(const Key& key, uint64_t value) override {
     std::stringstream ss;
-    ss << key.flat_key << " " << key.key << " " << std::hex
-       << reinterpret_cast<uintptr_t>(value) << std::dec;
+    ss << key.flat_key << " " << key.key << " " << std::hex << value
+       << std::dec;
     args_.push_back(ss.str());
   }
 
diff --git a/src/trace_processor/util/protozero_to_text.cc b/src/trace_processor/util/protozero_to_text.cc
index 269cb96..8ddc69f 100644
--- a/src/trace_processor/util/protozero_to_text.cc
+++ b/src/trace_processor/util/protozero_to_text.cc
@@ -15,8 +15,11 @@
  */
 
 #include "src/trace_processor/util/protozero_to_text.h"
+#include <cstdint>
 #include <optional>
+#include <string>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/proto_decoder.h"
@@ -27,9 +30,7 @@
 // This is the highest level that this protozero to text supports.
 #include "src/trace_processor/importers/proto/track_event.descriptor.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace protozero_to_text {
+namespace perfetto::trace_processor::protozero_to_text {
 
 namespace {
 
@@ -456,44 +457,6 @@
   return final_result;
 }
 
-std::string DebugTrackEventProtozeroToText(const std::string& type,
-                                           protozero::ConstBytes protobytes) {
-  DescriptorPool pool;
-  auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
-                                              kTrackEventDescriptor.size());
-  PERFETTO_DCHECK(status.ok());
-  return ProtozeroToText(pool, type, protobytes, kIncludeNewLines);
-}
-
-std::string ShortDebugTrackEventProtozeroToText(
-    const std::string& type,
-    protozero::ConstBytes protobytes) {
-  DescriptorPool pool;
-  auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
-                                              kTrackEventDescriptor.size());
-  PERFETTO_DCHECK(status.ok());
-  return ProtozeroToText(pool, type, protobytes, kSkipNewLines);
-}
-
-std::string ProtozeroEnumToText(const std::string& type, int32_t enum_value) {
-  DescriptorPool pool;
-  auto status = pool.AddFromFileDescriptorSet(kTrackEventDescriptor.data(),
-                                              kTrackEventDescriptor.size());
-  PERFETTO_DCHECK(status.ok());
-  auto opt_enum_descriptor_idx = pool.FindDescriptorIdx(type);
-  if (!opt_enum_descriptor_idx) {
-    // Fall back to the integer representation of the field.
-    return std::to_string(enum_value);
-  }
-  auto opt_enum_string =
-      pool.descriptors()[*opt_enum_descriptor_idx].FindEnumString(enum_value);
-  if (!opt_enum_string) {
-    // Fall back to the integer representation of the field.
-    return std::to_string(enum_value);
-  }
-  return *opt_enum_string;
-}
-
 std::string ProtozeroToText(const DescriptorPool& pool,
                             const std::string& type,
                             const std::vector<uint8_t>& protobytes,
@@ -503,6 +466,4 @@
       new_lines_mode);
 }
 
-}  // namespace protozero_to_text
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::protozero_to_text
diff --git a/src/trace_processor/util/protozero_to_text.h b/src/trace_processor/util/protozero_to_text.h
index 9a0acf1..43e961a 100644
--- a/src/trace_processor/util/protozero_to_text.h
+++ b/src/trace_processor/util/protozero_to_text.h
@@ -17,12 +17,13 @@
 #ifndef SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
 #define SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
 
+#include <cstdint>
 #include <string>
+#include <vector>
 
 #include "perfetto/protozero/field.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class DescriptorPool;
 
@@ -36,18 +37,6 @@
 };
 
 // Given a protozero message |protobytes| which is of fully qualified name
-// |type| within TrackEvent proto messages, we will convert this into a text
-// proto format string.
-//
-// DebugTrackEventProtozeroToText will use new lines between fields, and
-// ShortDebugTrackEventProtozeroToText will use only a single space.
-std::string DebugTrackEventProtozeroToText(const std::string& type,
-                                           protozero::ConstBytes protobytes);
-std::string ShortDebugTrackEventProtozeroToText(
-    const std::string& type,
-    protozero::ConstBytes protobytes);
-
-// Given a protozero message |protobytes| which is of fully qualified name
 // |type|, convert this into a text proto format string. All types used in
 // message definition of |type| must be available in |pool|.
 std::string ProtozeroToText(
@@ -62,17 +51,7 @@
                             const std::vector<uint8_t>& protobytes,
                             NewLinesMode new_lines_mode);
 
-// Allow the conversion from a protozero enum to a string. The template is just
-// to allow easy enum passing since we will do the explicit cast to a int32_t
-// for the user.
-std::string ProtozeroEnumToText(const std::string& type, int32_t enum_value);
-template <typename Enum>
-std::string ProtozeroEnumToText(const std::string& type, Enum enum_value) {
-  return ProtozeroEnumToText(type, static_cast<int32_t>(enum_value));
-}
-
 }  // namespace protozero_to_text
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_UTIL_PROTOZERO_TO_TEXT_H_
diff --git a/src/trace_processor/util/protozero_to_text_unittests.cc b/src/trace_processor/util/protozero_to_text_unittests.cc
index 232fc75..70cf42b 100644
--- a/src/trace_processor/util/protozero_to_text_unittests.cc
+++ b/src/trace_processor/util/protozero_to_text_unittests.cc
@@ -42,79 +42,6 @@
 using ::testing::Eq;
 using ::testing::StartsWith;
 
-TEST(ProtozeroToTextTest, TrackEventBasic) {
-  using perfetto::protos::pbzero::TrackEvent;
-  protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
-  msg->set_track_uuid(4);
-  msg->set_timestamp_delta_us(3);
-  auto binary_proto = msg.SerializeAsArray();
-  EXPECT_EQ(
-      "track_uuid: 4\ntimestamp_delta_us: 3",
-      DebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-  EXPECT_EQ(
-      "track_uuid: 4 timestamp_delta_us: 3",
-      ShortDebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
-TEST(ProtozeroToTextTest, TrackEventNestedMsg) {
-  using perfetto::protos::pbzero::TrackEvent;
-  protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
-  msg->set_track_uuid(4);
-  auto* state = msg->set_cc_scheduler_state();
-  state->set_deadline_us(7);
-  auto* machine = state->set_state_machine();
-  auto* minor_state = machine->set_minor_state();
-  minor_state->set_commit_count(8);
-  state->set_observing_begin_frame_source(true);
-  msg->set_timestamp_delta_us(3);
-  auto binary_proto = msg.SerializeAsArray();
-
-  EXPECT_EQ(
-      R"(track_uuid: 4
-cc_scheduler_state {
-  deadline_us: 7
-  state_machine {
-    minor_state {
-      commit_count: 8
-    }
-  }
-  observing_begin_frame_source: true
-}
-timestamp_delta_us: 3)",
-      DebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-
-  EXPECT_EQ(
-      "track_uuid: 4 cc_scheduler_state { deadline_us: 7 state_machine { "
-      "minor_state { commit_count: 8 } } observing_begin_frame_source: true } "
-      "timestamp_delta_us: 3",
-      ShortDebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
-TEST(ProtozeroToTextTest, TrackEventEnumNames) {
-  using perfetto::protos::pbzero::TrackEvent;
-  protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
-  msg->set_type(TrackEvent::TYPE_SLICE_BEGIN);
-  auto binary_proto = msg.SerializeAsArray();
-  EXPECT_EQ(
-      "type: TYPE_SLICE_BEGIN",
-      DebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-  EXPECT_EQ(
-      "type: TYPE_SLICE_BEGIN",
-      DebugTrackEventProtozeroToText(
-          ".perfetto.protos.TrackEvent",
-          protozero::ConstBytes{binary_proto.data(), binary_proto.size()}));
-}
-
 TEST(ProtozeroToTextTest, CustomDescriptorPoolBasic) {
   using perfetto::protos::pbzero::TrackEvent;
   protozero::HeapBuffered<TrackEvent> msg{kChunkSize, kChunkSize};
@@ -174,13 +101,6 @@
                       kSkipNewLines));
 }
 
-TEST(ProtozeroToTextTest, ProtozeroEnumToText) {
-  using perfetto::protos::pbzero::TrackEvent;
-  EXPECT_EQ("TYPE_SLICE_END",
-            ProtozeroEnumToText(".perfetto.protos.TrackEvent.Type",
-                                TrackEvent::TYPE_SLICE_END));
-}
-
 // Sets up a descriptor pool with all the messages from
 // "src/protozero/test/example_proto/test_messages.proto"
 class ProtozeroToTextTestMessageTest : public testing::Test {
diff --git a/src/trace_processor/util/regex.h b/src/trace_processor/util/regex.h
index 120e4c9..167af55 100644
--- a/src/trace_processor/util/regex.h
+++ b/src/trace_processor/util/regex.h
@@ -48,7 +48,7 @@
       regfree(&regex_.value());
     }
   }
-  Regex(Regex&) = delete;
+  Regex(const Regex&) = delete;
   Regex(Regex&& other) {
     regex_ = std::move(other.regex_);
     other.regex_ = std::nullopt;
diff --git a/src/trace_processor/util/status_macros.h b/src/trace_processor/util/status_macros.h
index c902bac..4412cd4 100644
--- a/src/trace_processor/util/status_macros.h
+++ b/src/trace_processor/util/status_macros.h
@@ -19,7 +19,7 @@
 
 #include "perfetto/trace_processor/status.h"
 
-// Evaluates |expr|, which should return a util::Status. If the status is an
+// Evaluates |expr|, which should return a base::Status. If the status is an
 // error status, returns the status from the current function.
 #define RETURN_IF_ERROR(expr)                           \
   do {                                                  \
diff --git a/src/traceconv/pprof_reader.cc b/src/traceconv/pprof_reader.cc
index f14cca7..612b7c7 100644
--- a/src/traceconv/pprof_reader.cc
+++ b/src/traceconv/pprof_reader.cc
@@ -16,8 +16,8 @@
 
 #include "src/traceconv/pprof_reader.h"
 
+#include <algorithm>
 #include <cinttypes>
-#include <fstream>
 
 #include "perfetto/ext/base/file_utils.h"
 
diff --git a/src/traceconv/trace_to_pprof_integrationtest.cc b/src/traceconv/trace_to_pprof_integrationtest.cc
index bff3e26..0f8812c 100644
--- a/src/traceconv/trace_to_pprof_integrationtest.cc
+++ b/src/traceconv/trace_to_pprof_integrationtest.cc
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-#include <unistd.h>
 #include "test/gtest_and_gmock.h"
 
 #include <fstream>
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index 3dd5962..296d150 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -547,7 +547,13 @@
   if (!data_sources_.empty())
     return;
 
-  if (!retain_ksyms_on_stop_) {
+  // The kernel symbol table is discarded by default to save memory as we run as
+  // a long-lived daemon. Check if the config asked to retain the symbols (e.g.
+  // lab tests). And in either case, reset a set-but-empty table to allow trying
+  // again next time a config requests symbols.
+  if (!retain_ksyms_on_stop_ ||
+      (symbolizer_.is_valid() &&
+       symbolizer_.GetOrCreateKernelSymbolMap()->num_syms() == 0)) {
     symbolizer_.Destroy();
   }
   retain_ksyms_on_stop_ = false;
@@ -608,7 +614,7 @@
   // buffers while doing the symbol parsing.
   if (data_source->config().symbolize_ksyms()) {
     symbolizer_.GetOrCreateKernelSymbolMap();
-    // If at least one config sets the KSYMS_RETAIN flag, keep the ksysm map
+    // If at least one config sets the KSYMS_RETAIN flag, keep the ksyms map
     // around in StopIfNeeded().
     const auto KRET = FtraceConfig::KSYMS_RETAIN;
     retain_ksyms_on_stop_ |= data_source->config().ksyms_mem_policy() == KRET;
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index fad6537..4bbfdf7 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -43,33 +43,16 @@
   shared_library("client_api_no_backends_compile_test") {
     deps = [
       ":client_api_without_backends",
-      ":platform_fake",
       "../../gn:default_deps",
     ]
   }
 }
 
-# Separate target because the embedder might not want this.
+# Some .gn build files outside of this repo (v8, webrtc) still reference this
+# target.
 source_set("platform_impl") {
-  deps = [
-    "../../gn:default_deps",
-    "../../include/perfetto/tracing",
-    "../base",
-  ]
-  sources = [
-    "platform_posix.cc",
-    "platform_windows.cc",
-  ]
-}
-
-# Fake platform that allows buiding the client lib on all OSes. You can only use
-# those parts of the client lib that do not use the platform.
-source_set("platform_fake") {
-  deps = [
-    "../../gn:default_deps",
-    "../../include/perfetto/tracing",
-  ]
-  sources = [ "platform_fake.cc" ]
+  deps = [ "../../gn:default_deps" ]
+  sources = []
 }
 
 # Code that both public headers and other non-public sources (e.g.
@@ -116,6 +99,8 @@
     "internal/track_event_internal.cc",
     "internal/track_event_interned_fields.cc",
     "platform.cc",
+    "platform_posix.cc",
+    "platform_windows.cc",
     "traced_value.cc",
     "tracing.cc",
     "tracing_policy.cc",
@@ -152,6 +137,7 @@
 perfetto_unittest_source_set("unittests") {
   testonly = true
   deps = [
+    ":client_api_without_backends",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
     "../../protos/perfetto/trace:lite",
@@ -163,19 +149,9 @@
 
   sources = []
 
-  # TODO(primiano): remove the build_with_chromium conditional once the root
-  # //BUILD.gn:libperfetto (in chromium) stops adding tracing:platform_fake.
-  # The problem is the following: in chrome builds we end up with duplicate
-  # symbol definitions in the test because both platforms (impl and fake) are
-  # present: impl added here and fake coming from chromium's base (full path:
-  # perfetto_unittests -> //(chromium)base:test_support -> //(chromium)base
-  # -> libperfetto -> platform_fake.
+  # TODO(lalitm): this tests appear to be failing on Chromium for unknown
+  # reasons. Figure out why and reenable them.
   if (!build_with_chromium) {
-    deps += [
-      ":client_api_without_backends",
-      ":platform_impl",
-    ]
-
     sources += [
       "internal/interceptor_trace_writer_unittest.cc",
       "traced_proto_unittest.cc",
@@ -234,7 +210,6 @@
   source_set("benchmarks") {
     testonly = true
     deps = [
-      ":platform_impl",
       "../..:libperfetto_client_experimental",
       "../../gn:benchmark",
       "../../gn:default_deps",
diff --git a/src/tracing/service/trace_buffer.cc b/src/tracing/service/trace_buffer.cc
index 69d66cc..111eecc 100644
--- a/src/tracing/service/trace_buffer.cc
+++ b/src/tracing/service/trace_buffer.cc
@@ -435,7 +435,8 @@
 
   for (size_t i = 0; i < patches_size; i++) {
     const size_t offset_untrusted = patches[i].offset_untrusted;
-    if (offset_untrusted > payload_size - Patch::kSize) {
+    if (payload_size < Patch::kSize ||
+        offset_untrusted > payload_size - Patch::kSize) {
       // Either the IPC was so slow and in the meantime the writer managed to
       // wrap over |chunk_id| or the producer sent a malicious IPC.
       stats_.set_patches_failed(stats_.patches_failed() + 1);
diff --git a/src/tracing/test/BUILD.gn b/src/tracing/test/BUILD.gn
index 3acedef..a562146 100644
--- a/src/tracing/test/BUILD.gn
+++ b/src/tracing/test/BUILD.gn
@@ -66,6 +66,7 @@
     testonly = true
     deps = [
       ":test_support",
+      "..:client_api_without_backends",
       "../../../gn:default_deps",
       "../../../gn:gtest_and_gmock",
       "../../base",
@@ -77,18 +78,9 @@
     ]
     sources = [ "tracing_integration_test.cc" ]
 
-    # TODO(primiano): remove the build_with_chromium conditional once the root
-    # //BUILD.gn:libperfetto (in chromium) stops adding tracing:platform_fake.
-    # The problem is the following: in chrome builds we end up with duplicate
-    # symbol definitions in the test because both platorm (impl and fake) are
-    # present: impl added here and fake coming from chromium's base (full path:
-    # perfetto_unittests -> //(chromium)base:test_support -> //(chromium)base
-    # -> libperfetto -> platform_fake.
+    # TODO(lalitm): this tests appear to be failing on Chromium for unknown
+    # reasons. Figure out why and reenable them.
     if (!build_with_chromium) {
-      deps += [
-        "..:client_api_without_backends",
-        "..:platform_impl",
-      ]
       sources += [ "platform_unittest.cc" ]
     }
   }
@@ -100,7 +92,6 @@
     deps = [
       ":api_test_support",
       "../:client_api",
-      "../:platform_impl",
       "../../../:libperfetto_client_experimental",
       "../../../gn:default_deps",
       "../../../gn:gtest_and_gmock",
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index 4ac5d4e..aceb540 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -213,20 +213,21 @@
 
 // static
 uint64_t TrackRegistry::ComputeProcessUuid() {
+  base::Hasher hash;
   // Use the process start time + pid as the unique identifier for this process.
   // This ensures that if there are two independent copies of the Perfetto SDK
   // in the same process (e.g., one in the app and another in a system
   // framework), events emitted by each will be consistently interleaved on
   // common thread and process tracks.
   if (uint64_t start_time = GetProcessStartTime()) {
-    base::Hasher hash;
     hash.Update(start_time);
-    hash.Update(Platform::GetCurrentProcessId());
-    return hash.digest();
+  } else {
+    // Fall back to a randomly generated identifier.
+    static uint64_t random_once = static_cast<uint64_t>(base::Uuidv4().lsb());
+    hash.Update(random_once);
   }
-  // Fall back to a randomly generated identifier.
-  static uint64_t random_once = static_cast<uint64_t>(base::Uuidv4().lsb());
-  return random_once;
+  hash.Update(Platform::GetCurrentProcessId());
+  return hash.digest();
 }
 
 void TrackRegistry::ResetForTesting() {
diff --git a/test/ci/bazel_tests.sh b/test/ci/bazel_tests.sh
index 54f5c6e..5db3200 100755
--- a/test/ci/bazel_tests.sh
+++ b/test/ci/bazel_tests.sh
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-INSTALL_BUILD_DEPS_ARGS=""
 source $(dirname ${BASH_SOURCE[0]})/common.sh
 
 # Save CI time by skipping runs on {UI,docs,infra}-only changes
@@ -23,8 +22,8 @@
 exit 0
 fi
 
-bazel build //:all --verbose_failures
-bazel build //python:all --verbose_failures
+tools/bazel build //:all --verbose_failures
+tools/bazel build //python:all --verbose_failures
 
 # Smoke test that processes run without crashing.
 ./bazel-bin/traced &
diff --git a/test/cts/BUILD.gn b/test/cts/BUILD.gn
index 3feee32..42db191 100644
--- a/test/cts/BUILD.gn
+++ b/test/cts/BUILD.gn
@@ -28,6 +28,7 @@
     "../../protos/perfetto/config/process_stats:cpp",
     "../../protos/perfetto/config/profiling:cpp",
     "../../protos/perfetto/trace:cpp",
+    "../../protos/perfetto/trace/interned_data:cpp",
     "../../protos/perfetto/trace/profiling:cpp",
     "../../src/base:test_support",
     "../../src/protozero/filtering:bytecode_generator",
diff --git a/test/cts/traced_perf_test_cts.cc b/test/cts/traced_perf_test_cts.cc
index 807c8e0..918ba88 100644
--- a/test/cts/traced_perf_test_cts.cc
+++ b/test/cts/traced_perf_test_cts.cc
@@ -32,6 +32,7 @@
 #include "protos/perfetto/common/perf_events.gen.h"
 #include "protos/perfetto/config/process_stats/process_stats_config.gen.h"
 #include "protos/perfetto/config/profiling/perf_event_config.gen.h"
+#include "protos/perfetto/trace/interned_data/interned_data.gen.h"
 #include "protos/perfetto/trace/profiling/profile_common.gen.h"
 #include "protos/perfetto/trace/profiling/profile_packet.gen.h"
 #include "protos/perfetto/trace/trace_packet.gen.h"
@@ -279,5 +280,74 @@
     AssertNoStacksForPid(packets, target_pid);
 }
 
+TEST(TracedPerfCtsTest, ProfileKernelCallstack) {
+  if (!HasPerfLsmHooks())
+    GTEST_SKIP() << "skipped due to lack of perf_event_open LSM hooks";
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);
+  trace_config.set_duration_ms(3000);
+  trace_config.set_data_source_stop_timeout_ms(8000);
+  trace_config.set_unique_session_name(RandomSessionName().c_str());
+
+  // Capture context switch callstacks from the rest of the device, as they have
+  // predictable function names for the test to assert.
+  protos::gen::PerfEventConfig perf_config;
+  auto* timebase = perf_config.mutable_timebase();
+  // We only need a few samples, and the kernel will record an early sample per
+  // core even at low "frequency" values.
+  timebase->set_frequency(1);
+  auto* tracepoint = timebase->mutable_tracepoint();
+  tracepoint->set_name("sched:sched_switch");
+
+  auto* callstacks = perf_config.mutable_callstack_sampling();
+  callstacks->set_kernel_frames(true);
+  callstacks->set_user_frames(protos::gen::PerfEventConfig::UNWIND_SKIP);
+
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("linux.perf");
+  ds_config->set_perf_event_config_raw(perf_config.SerializeAsString());
+
+  // Collect trace.
+  base::TestTaskRunner task_runner;
+  auto packets = CollectTrace(&task_runner, trace_config);
+
+  // Assert that we're seeing the symbolised scheduling functions, while
+  // double-checking that the interning ids are unrelated to the raw addresses.
+  const uint64_t kSmallInternedId = 4096;
+  size_t unexpected_iids = 0;
+
+  size_t fname_count = 0;
+  bool found_schedule = false;
+  for (const auto& packet : packets) {
+    for (const auto& fname : packet.interned_data().function_names()) {
+      fname_count++;
+      if (fname.str() == "schedule" || fname.str() == "__schedule" ||
+          fname.str() == "preempt_schedule") {
+        found_schedule = true;
+      }
+      if (fname.iid() > kSmallInternedId) {
+        unexpected_iids++;
+      }
+    }
+    for (const auto& frame : packet.interned_data().frames()) {
+      if (frame.iid() > kSmallInternedId) {
+        unexpected_iids++;
+      }
+    }
+    if (packet.has_perf_sample() &&
+        packet.perf_sample().callstack_iid() > kSmallInternedId) {
+      unexpected_iids++;
+    }
+  }
+
+  EXPECT_TRUE(found_schedule)
+      << "Failed to find a scheduling kernel function symbol name in the "
+         "profile, total functions seen: "
+      << fname_count;
+
+  EXPECT_EQ(unexpected_iids, 0u) << "Unexpectedly high interning ids.";
+}
+
 }  // namespace
 }  // namespace perfetto
diff --git a/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
index 9d4f6dc..f694f5d 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/frametimeline/frame-timeline-aggregation.png.sha256
@@ -1 +1 @@
-0b104eeb550b136333511b072be56dc3abf63664f88249db075a52afd3492592
\ No newline at end of file
+476be67ad67952c63eb73b5ad68d96be476f57bc835f826103586a508b522560
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
index fcf00cc..f029dce 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/gpu-counter/gpu-counter-aggregation.png.sha256
@@ -1 +1 @@
-dffb0fb8df8ce39dfc8ae01c1647ea30d93b190e527ff1ab87ed5590da80a9ea
\ No newline at end of file
+ac5cb189130bd8f57843c3b366ba8a12e1d27848d4722fb541c5405a3ef107d7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
index 856d657..23223bb 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-process.png.sha256
@@ -1 +1 @@
-06e371ba0b66007d781de10e76e6703518468fab9bc8c3d6c4c593b66dbd811b
\ No newline at end of file
+009ae9d78994c22adaa2484909acd8d2627f61b76573412012e079406cde7253
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
index dd957d9e..db33e76 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/cpu-by-thread.png.sha256
@@ -1 +1 @@
-6c82764ab14233d586c63b1aacef3f593b8359a39e3f369c3d2ae57cb04e16a6
\ No newline at end of file
+0889c04d6944c1704452294714be35dd1a044bf5eb2e85360d72143ba784b5d7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
index cf5c700..21bd7d9 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-occurrences.png.sha256
@@ -1 +1 @@
-0c0917bba2ce35b5f83833ea74200e4b19b58e75529e1087c08f6deb1d32a488
\ No newline at end of file
+b77e2ef33ad26be6ecd0a8ef71f87c69b488c4162d792f84f5204d4a8c0d95c5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
index 244ec4c..ad36d05 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration-desc.png.sha256
@@ -1 +1 @@
-8cacaa219ce05bc2d59a905512a8798bb2d857f5676097f30d10d1349bc48aa5
\ No newline at end of file
+f77cea1dfa259570c77ac7836adb998217af7c9a650479b6da6673a4d5f08096
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
index 6b6a626..d67923a 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/sched/sort-by-wall-duration.png.sha256
@@ -1 +1 @@
-0e96c5e7384488f90e21223d2db51719094eb7a180c8885a0250d26478a5c526
\ No newline at end of file
+c2e55d4a9f1671e0168637821dba9dbb77e67266cd10b20366084e0790aef290
\ No newline at end of file
diff --git a/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256 b/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
index 6270439..6ff98d0 100644
--- a/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
+++ b/test/data/ui-screenshots/aggregation.test.ts/slices/slice-aggregation.png.sha256
@@ -1 +1 @@
-c597b3f217765c7cd3b4e5db6bc2b2ae6aed643968960f19bd88d20050a96060
\ No newline at end of file
+78bf1ad90eaeb54628b3fcde2684a842dac3b5241cc55f98b5d00cd05f0e2f1c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256 b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
index f1bf92b..692a0bb 100644
--- a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/expand-all-tracks/all-tracks-expanded.png.sha256
@@ -1 +1 @@
-0133a9353c03118bc3c6737aa1e5b799e8cd888b4693300d9ac4bdbb72636863
\ No newline at end of file
+c91cf9293896ca4979eb1ecf0369c6fcf14816a33af01bc4cc21d1878cd4c187
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256 b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
index be184fa..6a4301f 100644
--- a/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
+++ b/test/data/ui-screenshots/chrome_missing_track_names.test.ts/trace-loaded/trace-loaded.png.sha256
@@ -1 +1 @@
-75e9f9fb872e1fe12b1aafa5428abf8695336b9602be66ff84f5b12fc70b5b9c
\ No newline at end of file
+e6ce2901471b1ef54fd2075f8ab9a0713d86766bff6951882ede3e8fbaaa65ad
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
index 4faf095..94e4723 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/expand-browser/browser-expanded.png.sha256
@@ -1 +1 @@
-9f9203330bdd5fd395c66dbdf5cb5056e711a243de9c2145db22f48b2c40662a
\ No newline at end of file
+1fb4597938818a3dfc4af7fee836738f42bbf74cc70cc88c7412567ecb93e5ea
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
index 1d4c10e..7276ab8 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-029dccb932965ac1aa292b86953a5ad3716a822f4f07d83d8124759ff106e040
\ No newline at end of file
+6333a4a7df4e5ca0471e3643bffd2b858ee70f618675dde459c704b086fa0e2c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256 b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
index 8053e36..4d73652 100644
--- a/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
+++ b/test/data/ui-screenshots/chrome_rendering_desktop.test.ts/slice-with-flows/slice-with-flows.png.sha256
@@ -1 +1 @@
-30b25f7f8a24ed7375b570ba70ee24902a8d409031ebf74b5227eeec052b55e9
\ No newline at end of file
+6e1b5bcc79a99f97b06b9581dd24f0c2e3eaac36dcfe975680ad5e071ba0f3ca
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
index fafbb80..372ff6b 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks-pivot/debug-track-pivot.png.sha256
@@ -1 +1 @@
-498b44ca27f6ae25bf5cc6c065b314ac685c9cd7d80675f68824ccd9ca939722
\ No newline at end of file
+1a0f90dd580562e442c7fbbfcdad37f9f9aeeb20d88c66b4f4deaa98e7630744
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
index 3d047f3..4bb3e55 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-slice-clicked.png.sha256
@@ -1 +1 @@
-40158e9d8e8b82f161fdedefb888ab998a16dee844894e3c1c8a0540b9d530a8
\ No newline at end of file
+2bbb39c5a1116587f8f511e2d2f346d6052efd796bb446e64f35fa83b34fc631
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
index 61c9caf..e532e33 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256
@@ -1 +1 @@
-d0b94afc57a69c00487918dd5a08c684b29fb530fe0edf0828d3b39c783dd9fa
\ No newline at end of file
+3907851e2b92ceb523d3ceb03b28003dfe7568d8431bf10a4997ca17ef9a6ba0
\ No newline at end of file
diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
index 6e60ba0..6d5456b 100644
--- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
+++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-removed.png.sha256
@@ -1 +1 @@
-9164ec2cc6c950abeae247982871a0175959acbb2b75b2b7a5dc715d91763455
\ No newline at end of file
+72287b646e7f912b752c806b84c610aa3188acde481a75b7e779f74825750e23
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
index a05f60c..6248292 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tab/ftrace-tab.png.sha256
@@ -1 +1 @@
-a3f878e50c3e342055a5f1ce3bc56bcc4d9be7852751eef8b301f5864a859dfa
\ No newline at end of file
+cee4518a2575d9cc929b8ba6c0409f7e7139a47da22ca7e7ab6c7658db275942
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
index 38dfbf6..0a0b087 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
@@ -1 +1 @@
-307d93331ab863a4e92dce150176b92536394d62894d35064c9aa24ac8fb5e1a
\ No newline at end of file
+201b85b1dc910a5956674b9a60b45d2bf96eaf0bd1cf25a2a3380c0ae8d1eee4
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
index f20a2ae..4616708 100644
--- a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip-expanded.png.sha256
@@ -1 +1 @@
-aa4ff3edb135b7f2210a3b136f8cd82e509a50cfc0617aff8a779f8adc8655ac
\ No newline at end of file
+041f3323e330c1e9b19bdcc306fbc6fc0aaf16288840e7d99e22968b12d57ad6
\ No newline at end of file
diff --git a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256 b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
index c01cf39..7102005 100644
--- a/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
+++ b/test/data/ui-screenshots/independent_features.test.ts/debuggable-chip/track-with-debuggable-chip.png.sha256
@@ -1 +1 @@
-d25beab0531610669dfa40e5d5c12412d6550f2db758accc386bd995c1d6f7cc
\ No newline at end of file
+8f2d538e455267467d91044a3d1f4e728fef93f91443a4265276bbe9cc3ebc8e
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
index 5f63afd..c417ca1 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/info-and-stats/back-to-timeline.png.sha256
@@ -1 +1 @@
-9f40fc0b8d54831ba0f0d9b9c39c7442d1e546ad0d51f1c0fafa9ee55c6a1609
\ No newline at end of file
+d34d9a1cdc9f65fafa41b838939ebf13aa408c7bd8d0b2e3bf7f1e4f9f57a9e8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
index 5f63afd..c417ca1 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-9f40fc0b8d54831ba0f0d9b9c39c7442d1e546ad0d51f1c0fafa9ee55c6a1609
\ No newline at end of file
+d34d9a1cdc9f65fafa41b838939ebf13aa408c7bd8d0b2e3bf7f1e4f9f57a9e8
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
index c116e4a..43ac071 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-0.png.sha256
@@ -1 +1 @@
-b3af6ddd9238a4712ea9ca96b3ed14dab13817b049dd701e6e7f64b5415d6c89
\ No newline at end of file
+44a410d14dcf3197ad29cc59623c8744717a517cf23d4ec85fd2b78926a343a7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
index c116e4a..43ac071 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-1.png.sha256
@@ -1 +1 @@
-b3af6ddd9238a4712ea9ca96b3ed14dab13817b049dd701e6e7f64b5415d6c89
\ No newline at end of file
+44a410d14dcf3197ad29cc59623c8744717a517cf23d4ec85fd2b78926a343a7
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
index 5a5853e..0437d0e 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-2.png.sha256
@@ -1 +1 @@
-2a37e0335d7dca1a5cc91a763e8fc2064e22a4504cfdbc9b05105613c0962333
\ No newline at end of file
+eeb4097bef65291e8741b016a327e2342eaca56a3265765288397b5c4349af78
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
index 620170b..03ad4aa 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/mark/mark-3.png.sha256
@@ -1 +1 @@
-0562e85510daf4c00a1dafb97f745b282e058989b25d1965627d672df32bb381
\ No newline at end of file
+5fce62f177042f1f16eea2fb83123ef01a53d9320a332a86df99855ea18d7e58
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
index ab2896e..f95582f 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/process-details.png.sha256
@@ -1 +1 @@
-7f46d1f492962572c6d99829f4e46604da437e3ffcf5c4c871854d2952fb7d8f
\ No newline at end of file
+66c2f109471f01b250328a21880c8d2393553f9b80459eb4e387bcdb36c78743
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
index 80822dc..ec62ebc 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/omnibox-search/search-slice.png.sha256
@@ -1 +1 @@
-dc636b348dee1f9d010ffc03b4eafb8622f19e5df5149559048f7dd81df4a8dc
\ No newline at end of file
+d7f69cde62ff76b61102c4fa029603a4efb0eb43b523585a5babe104e24bba4a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
index 0b394f0..d784edf 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/one-track-pinned.png.sha256
@@ -1 +1 @@
-cded4a4c2cfad1f7cd37a548a58fd78c0ac9f40938ec31072e4110001f35b807
\ No newline at end of file
+46dad937c32fc5ac1e2a5a4763f6815c64407caab0534e85d0fd2c684082e9ea
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
index 7391dd8..5280776 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/pin-tracks/two-tracks-pinned.png.sha256
@@ -1 +1 @@
-6091f10c58949550b7ad5bd5f68949dd5317ecbd5e393c48851636a782a943b1
\ No newline at end of file
+5e49f590943df485a954f4954e0cfe92544c937e6d8883f7aed9e65960f5aeac
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
index 3b7bb8c..c315f69 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-compressed.png.sha256
@@ -1 +1 @@
-9d24bd0ce0b301cba4026055a154e1a71ce30547e75b8db37ccb0c5eb0691207
\ No newline at end of file
+11d70ca250031414507c3275e004ffa99e0e19b303eb3d744d7f3910432aca17
\ No newline at end of file
diff --git a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256 b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
index 3b7bb8c..a8062ac 100644
--- a/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
+++ b/test/data/ui-screenshots/load_and_tracks.test.ts/track-expand-and-collapse/traced-probes-expanded.png.sha256
@@ -1 +1 @@
-9d24bd0ce0b301cba4026055a154e1a71ce30547e75b8db37ccb0c5eb0691207
\ No newline at end of file
+4c3c5c8441cb9c3fd05f7c14cba59354ff2ff95cc8b07cb10ab0c73d438ab1b1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
index dc4defa..534a03a 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/back-to-trace-1.png.sha256
@@ -1 +1 @@
-09c85f30096c40203d218215cf15288cce65baa648bce78d47433c6d22d70bd0
\ No newline at end of file
+d5d97f3c8f0f5db86f34edb5a94f8e5d49a7b8ddd917797fce1eb1276737b7cf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
index 7b10b88..8df0a78 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/confirmation-dialog.png.sha256
@@ -1 +1 @@
-d00d65fb1e0f4bb661a71e7e3cb7ae07ceeb1bc5925d49632fa932dd33fe9813
\ No newline at end of file
+e708381bdb43de90525020519a0b6c7f61af9c8ae08c2879e93bc24d44986e5b
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
index baf52b1..534a03a 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-1.png.sha256
@@ -1 +1 @@
-aa5ae8738d00562f3a0bad44ea0bea142c6669fb4d2518b75bf1464b31e442ea
\ No newline at end of file
+d5d97f3c8f0f5db86f34edb5a94f8e5d49a7b8ddd917797fce1eb1276737b7cf
\ No newline at end of file
diff --git a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256 b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
index b44bd50..1bd4993 100644
--- a/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
+++ b/test/data/ui-screenshots/local_cache_key.test.ts/multiple-traces-via-url-and-local-cache-key/trace-2.png.sha256
@@ -1 +1 @@
-fd84ea4faded9b6e777bd2a1498d02378068f9bc44c697617c4520e12a02a864
\ No newline at end of file
+afd50e2132446e616267ac176a1860e3d9e6b2de1077507ecd42f41054949eac
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
index 20bcf2f..8ea072b 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/omnibox-cleared.png.sha256
@@ -1 +1 @@
-21cd6ce57f0a7bbb3e1c683b6e3ef965b70eef223c83675a1908c78610d8e1b4
\ No newline at end of file
+9237e4e9027b64f60c6e36d44e6b0a32d6f2d4cc1ca3f7dad55c6b7b26dab0b1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
index 32aded1..485cbf4 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256
@@ -1 +1 @@
-f404c1b3b8feae5ed5c525e0f37fefc77f08721a192426ed75a50c1a811165f8
\ No newline at end of file
+ce0814c13403d7b21211cfc068ba152d54c8da03726e1732087f256a30260d09
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
index c42b448..8c556d9 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256
@@ -1 +1 @@
-3cf361f939b0a2056a81cffb5344e6cb8ad3b0d561ae6f8131164599c36a1a6c
\ No newline at end of file
+ee126bb2252e5d597ea8ccf5fe74d9ff8fef0021b41cd583ce9db66debbab6e5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
index 97f7a63..e578367 100644
--- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
+++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256
@@ -1 +1 @@
-e6298b572b9069e689af92597af259979d63746c1d68f70cb6f03821cd4f8ffc
\ No newline at end of file
+ae3f2d86686aff7e5085327eae154f301a7f9f6fdd214acf31484985e4de67ba
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
index 4b11647..fcc6ea2 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/ShowTable-command/slices-table.png.sha256
@@ -1 +1 @@
-4edd5cd26a4938b0d510ae4fb4ac62b55825e0d14f094a484bf332e58fb61a8f
\ No newline at end of file
+c39001db22206931e273678382447bb87b5a95e832a54bb861209c93af32a082
\ No newline at end of file
diff --git a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256 b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
index ba23b96..9ba0acf 100644
--- a/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
+++ b/test/data/ui-screenshots/sql_table_tab.test.ts/slices-with-same-name/slices-with-same-name.png.sha256
@@ -1 +1 @@
-550d2ec336bace4634a02f04b0bf5a5e828a918de0b4cb31dfd9c16ae20b29f6
\ No newline at end of file
+3f706d1008ac3fc3cdff6c900b5cd4b9c9d37ec02237b4575a58aaa277745eb5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
index adfaad5..8081e34 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/chronological-order/chronological.png.sha256
@@ -1 +1 @@
-e0ee27a824201ec74ecd8d490a4ff593a575f89be473a7c41d1fded7d7b4e30c
\ No newline at end of file
+31116f9523c9c99f936767da66b4d3b58c982cdb9971def21f7a561a2ae6ed73
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
index 96d28c3..6336d58 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/explicit-order/explicit.png.sha256
@@ -1 +1 @@
-f831590f6ecef858fa6f27db6a409c4f6af5bb31912250f36b06e82781981098
\ No newline at end of file
+d5342d1d3eb8de2d95596a94abdabab854befbf77c9d45ebfb2948693ec27620
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
index 9f5d054..804115a 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/lexicographic-tracks/lexicographic.png.sha256
@@ -1 +1 @@
-b53e700573ce7561da8f51ac094c16e8eb80ef43b56f861cd82ab47383f4a1a6
\ No newline at end of file
+c8d7378301534f1ebd1cbdf5da6bd9dca7b6be460bf81657a7a5baf2ecb769cd
\ No newline at end of file
diff --git a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256 b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
index 3cbc9eb..95024a1 100644
--- a/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
+++ b/test/data/ui-screenshots/track_event_ordered_tracks.test.ts/load-trace/loaded.png.sha256
@@ -1 +1 @@
-7d91add11aee7a160090a443e4b26543dbbdb49d9ace7d72a3f32aed61ba4a52
\ No newline at end of file
+ef1f15fba695a997b3eeec9faf6776a7c9b481d77ff3c2a5d7d2ef2ba06be71f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
index 610f226..42d5c45 100644
--- a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-process.png.sha256
@@ -1 +1 @@
-bdac97813abf1ef98f780fcf93d3f3b48c40cf807a2530e50d760bb7b9ac15dd
\ No newline at end of file
+84759b08519a616e0be568036aec73a90c603c73992416ef439e6db7d7a0bfc1
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
index 13270b5..8f0c982 100644
--- a/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/sched-aggregations/sched-aggr-thread.png.sha256
@@ -1 +1 @@
-c50394d0808fcb7909e8a491322676e9ae64148dc2c815f6d70250e0041c793d
\ No newline at end of file
+de4c43ba9570aebf4dfe964cde1802445856ab02812781ff368d4fcb259fb1f2
\ No newline at end of file
diff --git a/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256 b/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
index 09ceb1b..a1ecd5c 100644
--- a/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
+++ b/test/data/ui-screenshots/wattson.test.ts/wattson-aggregations/wattson-estimate-aggr.png.sha256
@@ -1 +1 @@
-0836aa16d7c78298d9563893e7155f591f19df6bd5cd03dd73a476fb3175293d
\ No newline at end of file
+bf940c29825fd4aa44cecc93bf146ee56f0e1013af4267ff5311da40a2b1adb9
\ No newline at end of file
diff --git a/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256 b/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
index cc5978e..18c7ea1 100644
--- a/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
+++ b/test/data/ui-screenshots/zoom.test.ts/zoom-in/zoomed-in.png.sha256
@@ -1 +1 @@
-7b179c70b90fbd8c4410a8c018ce6d53c93a5b7b311fbb42d9e61ba39a3c7090
\ No newline at end of file
+f7aaef8d41d81ee1b034b8788fd2f39abaa057a303769327eeb9c3c6883e4965
\ No newline at end of file
diff --git a/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256 b/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
index 4e405b8..d2c7787 100644
--- a/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
+++ b/test/data/ui-screenshots/zoom.test.ts/zoom-out/zoomed-out.png.sha256
@@ -1 +1 @@
-5370b83cf5d9935c3c470ddbe1c4cff10852e634314434a91378172365d6de6f
\ No newline at end of file
+d5532af59943fad497ab4bfb53fb30be76dc830eaddee3fa8484a4c0fd65dfeb
\ No newline at end of file
diff --git a/test/ftrace_integrationtest.cc b/test/ftrace_integrationtest.cc
index 4791b0d..3c14008 100644
--- a/test/ftrace_integrationtest.cc
+++ b/test/ftrace_integrationtest.cc
@@ -209,8 +209,7 @@
 // 1. On cuttlefish (x86-kvm). It's too slow when running on GCE (b/171771440).
 //    We cannot change the length of the production code in
 //    CanReadKernelSymbolAddresses() to deal with it.
-// 2. On user (i.e. non-userdebug) builds. As that doesn't work there by design.
-// 3. On ARM builds, because they fail on our CI.
+// 2. On ARM builds, because they fail on our CI.
 #if (PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD) && defined(__i386__)) || \
     defined(__arm__)
 #define MAYBE_KernelAddressSymbolization DISABLED_KernelAddressSymbolization
@@ -219,14 +218,10 @@
 #endif
 TEST_F(PerfettoFtraceIntegrationTest, MAYBE_KernelAddressSymbolization) {
   // On Android in-tree builds (TreeHugger): this test must always run to
-  // prevent selinux / property-related regressions. However it can run only on
-  // userdebug.
+  // prevent selinux / property-related regressions.
   // On standalone builds and Linux, this can be optionally skipped because
   // there it requires root to lower kptr_restrict.
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-  if (!IsDebuggableBuild())
-    GTEST_SKIP();
-#else
+#if !PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
   if (geteuid() != 0)
     GTEST_SKIP();
 #endif
diff --git a/test/synth_common.py b/test/synth_common.py
index c0355c3..cc2ae75 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -198,6 +198,9 @@
   def add_atrace_instant(self, ts, tid, pid, buf):
     self.add_print(ts, tid, 'I|{}|{}'.format(pid, buf))
 
+  def add_atrace_instant_for_track(self, ts, tid, pid, track_name, buf):
+        self.add_print(ts, tid, 'N|{}|{}|{}'.format(pid, track_name, buf))
+
   def add_process(self, pid, ppid, cmdline, uid=None):
     process = self.packet.process_tree.processes.add()
     process.pid = pid
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out
new file mode 100644
index 0000000..e413979
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.out
@@ -0,0 +1,45 @@
+android_blocking_calls_cuj_per_frame_metric {
+  cuj {
+    name: "BACK_PANEL_ARROW"
+    process {
+      name: "com.android.systemui"
+      uid: 10001
+      pid: 5000
+    }
+    blocking_calls {
+      name: "animation"
+      max_dur_per_frame_ms: 2
+      max_dur_per_frame_ns: 2000000
+      mean_dur_per_frame_ms: 1
+      mean_dur_per_frame_ns: 1000000
+      max_cnt_per_frame: 1
+      mean_cnt_per_frame: 0.5
+    }
+    blocking_calls {
+      name: "binder transaction"
+      max_dur_per_frame_ms: 2
+      max_dur_per_frame_ns: 2500000
+      mean_dur_per_frame_ms: 0
+      mean_dur_per_frame_ns: 750000
+      max_cnt_per_frame: 1
+      mean_cnt_per_frame: 0.5
+    }
+  }
+  cuj {
+    name: "CUJ_NAME"
+    process {
+      name: "com.google.android.apps.nexuslauncher"
+      uid: 10002
+      pid: 6000
+    }
+    blocking_calls {
+      name: "animation"
+      max_dur_per_frame_ms: 2
+      max_dur_per_frame_ns: 2000000
+      mean_dur_per_frame_ms: 1
+      mean_dur_per_frame_ns: 1500000
+      max_cnt_per_frame: 1
+      mean_cnt_per_frame: 1.0
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py
new file mode 100755
index 0000000..fa091ac
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_blocking_calls_cuj_per_frame_metric.py
@@ -0,0 +1,340 @@
+#!/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 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 synth_common
+from os import sys
+
+SYSUI_PID = 5000
+SYSUI_UI_TID = 5020
+LAUNCHER_PID = 6000
+LAUNCHER_UI_TID = 6020
+
+# RenderThread
+SYSUI_RTID = 1555
+LAUNCHER_RTID = 1655
+
+SYSUI_PACKAGE = "com.android.systemui"
+LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher"
+
+SYSUI_UID = 10001
+LAUNCHER_UID = 10002
+
+LAYER_1 = "TX - first_layer#0"
+LAYER_2 = "TX - second_layer#1"
+LAYER_3 = "TX - third_layer#2"
+
+FIRST_CUJ = "J<BACK_PANEL_ARROW>"
+SECOND_CUJ = "J<CUJ_NAME>"
+
+
+def add_async_trace(trace, ts, ts_end, buf, pid, tid):
+  trace.add_atrace_async_begin(ts=ts, tid=tid, pid=pid, buf=buf)
+  trace.add_atrace_async_end(ts=ts_end, tid=tid, pid=pid, buf=buf)
+
+def add_ui_thread_atrace(trace, ts, ts_end, buf, tid, pid):
+  trace.add_atrace_begin(ts=ts, tid=tid, pid=pid, buf=buf)
+  trace.add_atrace_end(ts=ts_end, tid=tid, pid=pid)
+
+
+def add_instant_event_in_thread(trace, ts, buf, pid, tid):
+  trace.add_atrace_instant(ts=ts, tid=tid, pid=pid, buf=buf)
+
+def add_render_thread_atrace_begin(trace, ts, buf, rtid, pid):
+  trace.add_atrace_begin(ts=ts, tid=rtid, pid=pid, buf=buf)
+
+
+def add_render_thread_atrace_end(trace, ts_end, rtid, pid):
+  trace.add_atrace_end(ts=ts_end, tid=rtid, pid=pid)
+
+
+def add_frame(trace, vsync, ts_do_frame, ts_end_do_frame, tid, pid):
+  add_ui_thread_atrace(
+      trace,
+      ts=ts_do_frame,
+      ts_end=ts_end_do_frame,
+      buf="Choreographer#doFrame %d" % vsync,
+      tid=tid,
+      pid=pid)
+
+def add_expected_surface_frame_events(ts, dur, token, pid):
+  trace.add_expected_surface_frame_start_event(
+      ts=ts,
+      cookie=100000 + token,
+      token=token,
+      display_frame_token=100 + token,
+      pid=pid,
+      layer_name='')
+  trace.add_frame_end_event(ts=ts + dur, cookie=100000 + token)
+
+def add_actual_surface_frame_events(ts, dur, token, layer, pid):
+  cookie = token + 1
+  trace.add_actual_surface_frame_start_event(
+      ts=ts,
+      cookie=100002 + cookie,
+      token=token,
+      display_frame_token=token + 100,
+      pid=pid,
+      present_type=1,
+      on_time_finish=1,
+      gpu_composition=0,
+      jank_type=1,
+      prediction_type=3,
+      layer_name=layer)
+  trace.add_frame_end_event(ts=ts + dur, cookie=100002 + cookie)
+
+def add_blocking_calls_per_frame_multiple_cuj_instance(trace, cuj_name):
+
+  # add a new CUJ in trace.
+  add_async_trace(trace, ts=25_000_000, ts_end=77_000_000, buf=cuj_name, pid=SYSUI_PID, tid=SYSUI_UI_TID)
+  add_async_trace(trace, ts=83_000_000, ts_end=102_000_000, buf=cuj_name, pid=SYSUI_PID, tid=SYSUI_UI_TID)
+
+  trace.add_atrace_instant_for_track(ts=25_000_001,
+                                               buf="FT#beginVsync#20",
+                                               pid=SYSUI_PID,
+                                               tid=SYSUI_UI_TID,
+                                               track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=25_000_010,
+                                                   buf="FT#layerId#0",
+                                                   pid=SYSUI_PID,
+                                                   tid=SYSUI_UI_TID,
+                                                   track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=76_000_001,
+                                                 buf="FT#endVsync#30",
+                                                 pid=SYSUI_PID,
+                                                 tid=SYSUI_UI_TID,
+                                                 track_name=cuj_name)
+
+
+  trace.add_atrace_instant_for_track(ts=83_000_001,
+                                               buf="FT#beginVsync#60",
+                                               pid=SYSUI_PID,
+                                               tid=SYSUI_UI_TID,
+                                               track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=83_000_010,
+                                                   buf="FT#layerId#2",
+                                                   pid=SYSUI_PID,
+                                                   tid=SYSUI_UI_TID,
+                                                   track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=101_000_001,
+                                                 buf="FT#endVsync#70",
+                                                 pid=SYSUI_PID,
+                                                 tid=SYSUI_UI_TID,
+                                                 track_name=cuj_name)
+
+  # Add Choreographer#doFrame outside CUJ boundary. This frame will not be considered during
+  # metric calculation.
+  add_frame(
+          trace,
+          vsync=15,
+          ts_do_frame=9_000_000,
+          ts_end_do_frame=15_000_000,
+          tid=SYSUI_UI_TID,
+          pid=SYSUI_PID)
+
+  add_render_thread_atrace_begin(trace, ts=10_000_000, buf="DrawFrames 15", rtid=SYSUI_RTID, pid=SYSUI_PID)
+  add_render_thread_atrace_end(trace, ts_end=12_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+  # Add Choreographer#doFrame slices within CUJ boundary.
+  add_frame(
+        trace,
+        vsync=20,
+        ts_do_frame=26_000_000,
+        ts_end_do_frame=32_000_000,
+        tid=SYSUI_UI_TID,
+        pid=SYSUI_PID)
+
+  add_render_thread_atrace_begin(trace, ts=27_000_000, buf="DrawFrames 20", rtid=SYSUI_RTID, pid=SYSUI_PID)
+  add_render_thread_atrace_end(trace, ts_end=28_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+  add_frame(
+        trace,
+        vsync=22,
+        ts_do_frame=43_000_000,
+        ts_end_do_frame=49_000_000,
+        tid=SYSUI_UI_TID,
+        pid=SYSUI_PID)
+
+  add_render_thread_atrace_begin(trace, ts=44_000_000, buf="DrawFrames 22", rtid=SYSUI_RTID, pid=SYSUI_PID)
+  add_render_thread_atrace_end(trace, ts_end=45_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+  add_frame(
+          trace,
+          vsync=24,
+          ts_do_frame=60_000_000,
+          ts_end_do_frame=65_000_000,
+          tid=SYSUI_UI_TID,
+          pid=SYSUI_PID)
+
+  add_render_thread_atrace_begin(trace, ts=61_000_000, buf="DrawFrames 24", rtid=SYSUI_RTID, pid=SYSUI_PID)
+  add_render_thread_atrace_end(trace, ts_end=62_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+  add_frame(
+          trace,
+          vsync=65,
+          ts_do_frame=84_000_000,
+          ts_end_do_frame=89_000_000,
+          tid=SYSUI_UI_TID,
+          pid=SYSUI_PID)
+
+  add_render_thread_atrace_begin(trace, ts=85_000_000, buf="DrawFrames 65", rtid=SYSUI_RTID, pid=SYSUI_PID)
+  add_render_thread_atrace_end(trace, ts_end=86_000_000, rtid=SYSUI_RTID, pid=SYSUI_PID)
+
+  trace.add_atrace_begin(
+          ts=28_000_000, buf="binder transaction", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+  trace.add_atrace_end(ts=28_500_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+  trace.add_atrace_begin(
+          ts=30_000_000, buf="animation", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+  trace.add_atrace_end(ts=32_000_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+  trace.add_atrace_begin(
+          ts=62_000_000, buf="animation", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+  trace.add_atrace_end(ts=64_000_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+  trace.add_atrace_begin(
+          ts=86_000_000, buf="binder transaction", tid=SYSUI_UI_TID, pid=SYSUI_PID)
+  trace.add_atrace_end(ts=88_500_000, tid=SYSUI_UI_TID, pid=SYSUI_PID)
+
+  # Add expected and actual frames.
+  add_expected_surface_frame_events(ts=10_000_000, dur=16_000_000, token=15, pid=SYSUI_PID)
+  add_actual_surface_frame_events(ts=10_000_000, dur=16_000_000, token=15, layer=LAYER_1, pid=SYSUI_PID)
+
+  add_expected_surface_frame_events(ts=27_000_000, dur=16_000_000, token=20, pid=SYSUI_PID)
+  add_actual_surface_frame_events(ts=27_000_000, dur=7_000_000, token=20, layer=LAYER_1, pid=SYSUI_PID)
+
+  add_expected_surface_frame_events(ts=44_000_000, dur=16_000_000, token=22, pid=SYSUI_PID)
+  add_actual_surface_frame_events(ts=44_000_000, dur=7_000_000, token=22, layer=LAYER_1, pid=SYSUI_PID)
+
+  add_expected_surface_frame_events(ts=61_000_000, dur=16_000_000, token=24, pid=SYSUI_PID)
+  add_actual_surface_frame_events(ts=61_000_000, dur=6_000_000, token=24, layer=LAYER_1, pid=SYSUI_PID)
+
+  add_expected_surface_frame_events(ts=85_000_000, dur=16_000_000, token=65, pid=SYSUI_PID)
+  add_actual_surface_frame_events(ts=85_000_000, dur=6_000_000, token=65, layer=LAYER_3, pid=SYSUI_PID)
+
+def add_blocking_call_crossing_frame_boundary(trace, cuj_name):
+
+  # add a new CUJ in trace.
+  add_async_trace(trace, ts=120_000_000, ts_end=145_000_000, buf=cuj_name, pid=LAUNCHER_PID, tid=LAUNCHER_UI_TID)
+
+  add_instant_event_in_thread(
+        trace,
+        ts=120_000_001,
+        buf=cuj_name + "#UIThread",
+        pid=LAUNCHER_PID,
+        tid=LAUNCHER_UI_TID)
+
+  trace.add_atrace_instant_for_track(ts=120_000_002,
+                                                 buf="FT#beginVsync#80",
+                                                 pid=LAUNCHER_PID,
+                                                 tid=LAUNCHER_UI_TID,
+                                                 track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=120_000_010,
+                                                 buf="FT#layerId#1",
+                                                 pid=LAUNCHER_PID,
+                                                 tid=LAUNCHER_UI_TID,
+                                                 track_name=cuj_name)
+
+  trace.add_atrace_instant_for_track(ts=144_000_001,
+                                                 buf="FT#endVsync#90",
+                                                 pid=LAUNCHER_PID,
+                                                 tid=LAUNCHER_UI_TID,
+                                                 track_name=cuj_name)
+
+
+  # Add Choreographer#doFrame outside CUJ boundary. This frame will not be considered during
+    # metric calculation.
+  add_frame(
+            trace,
+            vsync=75,
+            ts_do_frame=103_000_000,
+            ts_end_do_frame=110_000_000,
+            tid=LAUNCHER_UI_TID,
+            pid=LAUNCHER_PID)
+
+  add_render_thread_atrace_begin(trace, ts=104_000_000, buf="DrawFrames 75", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+  add_render_thread_atrace_end(trace, ts_end=105_000_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+    # Add Choreographer#doFrame slices within CUJ boundary.
+  add_frame(
+            trace,
+            vsync=80,
+            ts_do_frame=120_000_000,
+            ts_end_do_frame=126_000_000,
+            tid=LAUNCHER_UI_TID,
+            pid=LAUNCHER_PID)
+
+  add_render_thread_atrace_begin(trace, ts=121_000_000, buf="DrawFrames 80", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+  add_render_thread_atrace_end(trace, ts_end=122_000_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+  add_frame(
+            trace,
+            vsync=82,
+            ts_do_frame=141_000_000,
+            ts_end_do_frame=143_000_000,
+            tid=LAUNCHER_UI_TID,
+            pid=LAUNCHER_PID)
+
+  add_render_thread_atrace_begin(trace, ts=142_000_000, buf="DrawFrames 82", rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+  add_render_thread_atrace_end(trace, ts_end=142_500_000, rtid=LAUNCHER_RTID, pid=LAUNCHER_PID)
+
+  trace.add_atrace_begin(
+        ts=127_000_000, buf="animation", tid=LAUNCHER_UI_TID, pid=LAUNCHER_PID)
+  trace.add_atrace_end(ts=140_000_000, tid=LAUNCHER_UI_TID, pid=LAUNCHER_PID)
+
+  # Add expected and actual frames.
+  add_expected_surface_frame_events(ts=104_000_000, dur=16_000_000, token=75, pid=LAUNCHER_PID)
+  add_actual_surface_frame_events(ts=104_000_000, dur=16_000_000, token=75, layer=LAYER_2, pid=LAUNCHER_PID)
+
+  add_expected_surface_frame_events(ts=121_000_000, dur=16_000_000, token=80, pid=LAUNCHER_PID)
+  add_actual_surface_frame_events(ts=121_000_000, dur=7_000_000, token=80, layer=LAYER_2, pid=LAUNCHER_PID)
+
+  add_expected_surface_frame_events(ts=138_000_000, dur=16_000_000, token=82, pid=LAUNCHER_PID)
+  add_actual_surface_frame_events(ts=138_000_000, dur=6_000_000, token=82, layer=LAYER_2, pid=LAUNCHER_PID)
+
+def add_process(trace, package_name, uid, pid):
+  trace.add_package_list(ts=0, name=package_name, uid=uid, version_code=1)
+  trace.add_process(pid=pid, ppid=0, cmdline=package_name, uid=uid)
+  trace.add_thread(tid=pid, tgid=pid, cmdline="MainThread", name="MainThread")
+
+def setup_trace():
+  trace = synth_common.create_trace()
+  trace.add_packet()
+  add_process(
+        trace, package_name=SYSUI_PACKAGE, uid=SYSUI_UID, pid=SYSUI_PID)
+  add_process(
+        trace,
+        package_name=LAUNCHER_PACKAGE,
+        uid=LAUNCHER_UID,
+        pid=LAUNCHER_PID)
+
+  trace.add_thread(
+      tid=SYSUI_UI_TID,
+      tgid=SYSUI_PID,
+      cmdline="BackPanelUiThre",
+      name="BackPanelUiThre")
+
+  trace.add_ftrace_packet(cpu=0)
+  return trace
+
+trace = setup_trace()
+add_blocking_calls_per_frame_multiple_cuj_instance(trace, FIRST_CUJ)
+trace.add_ftrace_packet(cpu=0)
+add_blocking_call_crossing_frame_boundary(trace, SECOND_CUJ)
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index 5c1e0ba..15925a1 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -145,6 +145,12 @@
         query=Metric('android_blocking_calls_cuj_metric'),
         out=Path('android_blocking_calls_cuj_different_ui_thread.out'))
 
+  def test_android_blocking_calls_cuj_per_frame(self):
+      return DiffTestBlueprint(
+          trace=Path('android_blocking_calls_cuj_per_frame_metric.py'),
+          query=Metric('android_blocking_calls_cuj_per_frame_metric'),
+          out=Path('android_blocking_calls_cuj_per_frame_metric.out'))
+
   def test_sysui_notif_shade_list_builder(self):
     return DiffTestBlueprint(
         trace=Path('android_sysui_notif_shade_list_builder_metric.py'),
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
index d1115cf..63bd6df 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range.textproto
@@ -13,7 +13,7 @@
       pid: 1
       tid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
@@ -26,7 +26,7 @@
       pid: 2
       tid: 2
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
index f02a3e6..cce038c 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_cropping.textproto
@@ -20,7 +20,7 @@
       pid: 1
       tid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
@@ -33,7 +33,7 @@
       pid: 2
       tid: 2
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
@@ -47,7 +47,7 @@
       pid: 3
       tid: 3
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
index b7dac0b..9605a6d 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_browser_main.textproto
@@ -11,19 +11,19 @@
       pid: 1
       tid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
     timestamp: 2
     track_descriptor {
-        uuid: 2
-        process {
-            pid: 1
-        }
-        chrome_process {
-            process_type: PROCESS_BROWSER
-        }
+      uuid: 2
+      process {
+        pid: 1
+      }
+      chrome_process {
+        process_type: PROCESS_BROWSER
+      }
     }
 }
 
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
index 4465a30..933f549 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_gpu_main.textproto
@@ -11,19 +11,19 @@
       pid: 1
       tid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
     timestamp: 2
     track_descriptor {
-        uuid: 2
-        process {
-            pid: 1
-        }
-        chrome_process {
-            process_type: PROCESS_GPU
-        }
+      uuid: 2
+      process {
+          pid: 1
+      }
+      chrome_process {
+          process_type: PROCESS_GPU
+      }
     }
 }
 
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
index ffc093c..a42f47f 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_processes.textproto
@@ -24,9 +24,9 @@
       tid: 1
     }
     process {
-        pid: 1
+      pid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 
diff --git a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
index 42cb741..6cee9c3 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
+++ b/test/trace_processor/diff_tests/metrics/chrome/chrome_reliable_range_missing_renderer_main.textproto
@@ -11,19 +11,19 @@
       pid: 1
       tid: 1
     }
-    parent_uuid: 0
+    disallow_merging_with_system_tracks: true
   }
 }
 packet {
     timestamp: 2
     track_descriptor {
-        uuid: 2
-        process {
-            pid: 1
-        }
-        chrome_process {
-            process_type: PROCESS_RENDERER
-        }
+      uuid: 2
+      process {
+        pid: 1
+      }
+      chrome_process {
+        process_type: PROCESS_RENDERER
+      }
     }
 }
 
diff --git a/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out b/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out
new file mode 100644
index 0000000..edc1d93
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/etm/iterate_instructions.out
@@ -0,0 +1,18 @@
+"element_index","instruction_index","address","opcode","type","branch_address","is_conditional","is_link","sub_type"
+7,0,434500225096,335544321,"BR",434500225100,0,0,"NONE"
+8,0,434500225100,1384120360,"OTHER","[NULL]",0,0,"NONE"
+8,1,434500225104,923271016,"BR",434500225084,1,0,"NONE"
+9,0,434500225084,2483027979,"BR",434500225128,0,1,"BR_LINK"
+10,0,434500225128,3506471935,"OTHER","[NULL]",0,0,"NONE"
+10,1,434500225132,2835446781,"OTHER","[NULL]",0,0,"NONE"
+10,2,434500225136,2432713725,"OTHER","[NULL]",0,0,"NONE"
+10,3,434500225140,3577471048,"OTHER","[NULL]",0,0,"NONE"
+10,4,434500225144,4177528808,"OTHER","[NULL]",0,0,"NONE"
+10,5,434500225148,4181723105,"OTHER","[NULL]",0,0,"NONE"
+10,6,434500225152,3573751839,"OTHER","[NULL]",0,0,"NONE"
+10,7,434500225156,285076384,"OTHER","[NULL]",0,0,"NONE"
+10,8,434500225160,2483028070,"BR",434500225568,0,1,"BR_LINK"
+11,0,434500225568,2415919152,"OTHER","[NULL]",0,0,"NONE"
+11,1,434500225572,4182138385,"OTHER","[NULL]",0,0,"NONE"
+11,2,434500225576,2436030992,"OTHER","[NULL]",0,0,"NONE"
+11,3,434500225580,3592356384,"BR_INDIRECT",434500225568,0,0,"NONE"
diff --git a/test/trace_processor/diff_tests/parser/etm/tests.py b/test/trace_processor/diff_tests/parser/etm/tests.py
index 8aaac9a..e1b4c77 100644
--- a/test/trace_processor/diff_tests/parser/etm/tests.py
+++ b/test/trace_processor/diff_tests/parser/etm/tests.py
@@ -78,3 +78,17 @@
           WHERE trace_id = 0
         ''',
         out=Path('decode_trace.out'))
+
+  def test_iterate_instructions(self):
+    return DiffTestBlueprint(
+        register_files_dir=DataPath('simpleperf/bin'),
+        trace=DataPath('simpleperf/cs_etm_u.perf'),
+        query='''
+          SELECT d.element_index, i.*
+          FROM
+            __intrinsic_etm_decode_trace d,
+            __intrinsic_etm_iterate_instruction_range i
+            USING(instruction_range)
+          WHERE trace_id = 0 AND mapping_id = 1
+        ''',
+        out=Path('iterate_instructions.out'))
diff --git a/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql b/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql
deleted file mode 100644
index 269523c..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/actual_frame_timeline_events_test.sql
+++ /dev/null
@@ -1,24 +0,0 @@
---
--- Copyright 2020 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
-
-SELECT ts, dur, process.pid, display_frame_token, surface_frame_token, layer_name,
-  present_type, on_time_finish, gpu_composition, jank_type, prediction_type, jank_tag, jank_severity_type
-FROM
-  (SELECT t.*, process_track.name AS track_name FROM
-    process_track LEFT JOIN actual_frame_timeline_slice t
-    ON process_track.id = t.track_id) s
-JOIN process USING(upid)
-WHERE s.track_name = 'Actual Timeline'
-ORDER BY ts;
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out
deleted file mode 100644
index da72f32..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages.out
+++ /dev/null
@@ -1,22 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
-"queue 1","queue desc 1",0,5,"render stage(1)",0,"[NULL]","[NULL]",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"queue 0","queue desc 0",0,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 1","queue desc 1",10,5,"stage 1",0,"description","stage desc 1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"queue 2","[NULL]",20,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
-"queue 0","queue desc 0",30,5,"stage 3",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"Unknown GPU Queue 3","[NULL]",40,5,"render stage(4)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,3,"[NULL]"
-"queue 0","queue desc 0",50,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",60,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",60,5,"stage 0",0,"key2","value2",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",70,5,"stage 0",0,"key1","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",80,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",90,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",32,"[NULL]",48,"[NULL]",0,0,"[NULL]"
-"queue 0","queue desc 0",100,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"[NULL]",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",110,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",120,5,"stage 0",0,"[NULL]","[NULL]",42,16,"framebuffer",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
-"queue 0","queue desc 0",130,5,"stage 0",0,"[NULL]","[NULL]",42,16,"renamed_buffer",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
-"Unknown GPU Queue ","[NULL]",140,5,"render stage(18446744073709551615)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1024,"[NULL]"
-"queue 0","queue desc 0",150,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"0"
-"queue 0","queue desc 0",160,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"63,64"
-"queue 0","queue desc 0",170,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"64"
-"queue 0","queue desc 0",180,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"3,68,69,70,71"
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out
deleted file mode 100644
index 05afa4d..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_interned_spec.out
+++ /dev/null
@@ -1,4 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
-"vertex","vertex queue",100,10,"binning",0,"description","binning graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
-"fragment","fragment queue",200,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
-"queue2","queue2 description",300,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql b/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql
deleted file mode 100644
index bac5239..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/gpu_render_stages_test.sql
+++ /dev/null
@@ -1,24 +0,0 @@
---
--- Copyright 2019 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     https://www.apache.org/licenses/LICENSE-2.0
---
--- Unless required by applicable law or agreed to in writing, software
--- distributed under the License is distributed on an "AS IS" BASIS,
--- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--- See the License for the specific language governing permissions and
--- limitations under the License.
---
-SELECT track.name AS track_name, gpu_track.description AS track_desc, ts, dur,
-  gpu_slice.name AS slice_name, depth, flat_key, string_value,
-  gpu_slice.context_id, render_target, render_target_name, render_pass, render_pass_name,
-  command_buffer, command_buffer_name, submission_id, hw_queue_id, render_subpasses
-FROM gpu_track
-LEFT JOIN track USING (id)
-JOIN gpu_slice ON gpu_track.id = gpu_slice.track_id
-LEFT JOIN args ON gpu_slice.arg_set_id = args.arg_set_id
-ORDER BY ts;
diff --git a/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out b/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out
deleted file mode 100644
index be781b8..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/graphics_frame_events.out
+++ /dev/null
@@ -1,50 +0,0 @@
-"ts","track_name","dur","slice_name","frame_number","layer_name"
-1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
-1,"APP_1 layer1",3,"11",11,"layer1"
-4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
-4,"GPU_1 layer1",2,"11",11,"layer1"
-6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
-6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
-6,"APP_2 layer2",3,"12",12,"layer2"
-7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
-8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
-8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
-8,"SF_1 layer1",6,"11",11,"layer1"
-9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
-11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
-11,"SF_2 layer2",5,"12",12,"layer2"
-14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
-14,"Display_layer1",10,"11",11,"layer1"
-16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
-16,"Display_layer2",-1,"12",12,"layer2"
-24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
-24,"Display_layer1",-1,"13",13,"layer1"
-31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
-31,"APP_1 layer1",3,"21",21,"layer1"
-34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
-34,"GPU_1 layer1",-1,"21",21,"layer1"
-37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
-37,"APP_1 layer1",4,"22",22,"layer1"
-41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
-41,"GPU_1 layer1",5,"22",22,"layer1"
-46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
-53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
-53,"APP_2 layer2",-1,"0",0,"layer2"
-59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
-61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
-61,"SF_2 layer2",-1,"24",24,"layer2"
-63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
-63,"APP_1 layer1",-1,"0",0,"layer1"
-73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
-73,"APP_1 layer1",2,"26",26,"layer1"
-75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
-75,"GPU_1 layer1",4,"26",26,"layer1"
-79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
-81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
-81,"APP_1 layer1",2,"30",30,"layer1"
-83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
-83,"GPU_1 layer1",-1,"30",30,"layer1"
-90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
-90,"APP_1 layer2",2,"35",35,"layer2"
-92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
-92,"GPU_1 layer2",-1,"35",35,"layer2"
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests.py b/test/trace_processor/diff_tests/parser/graphics/tests.py
index f92264a..7da3eb0 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests.py
@@ -33,7 +33,58 @@
         WHERE scope = 'graphics_frame_event'
         ORDER BY ts;
         """,
-        out=Path('graphics_frame_events.out'))
+        out=Csv('''
+          "ts","track_name","dur","slice_name","frame_number","layer_name"
+          1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
+          1,"APP_1 layer1",3,"11",11,"layer1"
+          4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
+          4,"GPU_1 layer1",2,"11",11,"layer1"
+          6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
+          6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
+          6,"APP_2 layer2",3,"12",12,"layer2"
+          7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
+          8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
+          8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
+          8,"SF_1 layer1",6,"11",11,"layer1"
+          9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
+          11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
+          11,"SF_2 layer2",5,"12",12,"layer2"
+          14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
+          14,"Display_layer1",10,"11",11,"layer1"
+          16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
+          16,"Display_layer2",-1,"12",12,"layer2"
+          24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
+          24,"Display_layer1",-1,"13",13,"layer1"
+          31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
+          31,"APP_1 layer1",3,"21",21,"layer1"
+          34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
+          34,"GPU_1 layer1",-1,"21",21,"layer1"
+          37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
+          37,"APP_1 layer1",4,"22",22,"layer1"
+          41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
+          41,"GPU_1 layer1",5,"22",22,"layer1"
+          46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
+          53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
+          53,"APP_2 layer2",-1,"0",0,"layer2"
+          59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
+          61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
+          61,"SF_2 layer2",-1,"24",24,"layer2"
+          63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
+          63,"APP_1 layer1",-1,"0",0,"layer1"
+          73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
+          73,"APP_1 layer1",2,"26",26,"layer1"
+          75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
+          75,"GPU_1 layer1",4,"26",26,"layer1"
+          79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
+          81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
+          81,"APP_1 layer1",2,"30",30,"layer1"
+          83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
+          83,"GPU_1 layer1",-1,"30",30,"layer1"
+          90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
+          90,"APP_1 layer2",2,"35",35,"layer2"
+          92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
+          92,"GPU_1 layer2",-1,"35",35,"layer2"
+        '''))
 
   # GPU Memory ftrace packets
   def test_gpu_mem_total(self):
@@ -104,42 +155,52 @@
         query=Path('expected_frame_timeline_events_test.sql'),
         out=Csv("""
         "ts","dur","pid","display_frame_token","surface_frame_token","layer_name"
-        20,6,666,2,0,"[NULL]"
+        20,6,666,2,"[NULL]","[NULL]"
         21,15,1000,4,1,"Layer1"
-        40,6,666,4,0,"[NULL]"
+        40,6,666,4,"[NULL]","[NULL]"
         41,15,1000,6,5,"Layer1"
-        80,6,666,6,0,"[NULL]"
+        80,6,666,6,"[NULL]","[NULL]"
         90,16,1000,8,7,"Layer1"
-        120,6,666,8,0,"[NULL]"
-        140,6,666,12,0,"[NULL]"
+        120,6,666,8,"[NULL]","[NULL]"
+        140,6,666,12,"[NULL]","[NULL]"
         150,20,1000,15,14,"Layer1"
-        170,6,666,15,0,"[NULL]"
-        200,6,666,17,0,"[NULL]"
-        220,-1,666,18,0,"[NULL]"
-        220,10,666,18,0,"[NULL]"
+        170,6,666,15,"[NULL]","[NULL]"
+        200,6,666,17,"[NULL]","[NULL]"
+        220,-1,666,18,"[NULL]","[NULL]"
+        220,10,666,18,"[NULL]","[NULL]"
         """))
 
   def test_actual_frame_timeline_events(self):
     return DiffTestBlueprint(
         trace=Path('frame_timeline_events.py'),
-        query=Path('actual_frame_timeline_events_test.sql'),
+        query='''
+          SELECT ts, dur, process.pid, display_frame_token, surface_frame_token, layer_name,
+            present_type, on_time_finish, gpu_composition, jank_type, prediction_type, jank_tag, jank_severity_type
+          FROM
+            (SELECT t.*, process_track.name AS track_name FROM
+              process_track LEFT JOIN actual_frame_timeline_slice t
+              ON process_track.id = t.track_id) s
+          JOIN process USING(upid)
+          WHERE s.track_name = 'Actual Timeline'
+          ORDER BY ts;
+        ''',
         out=Csv("""
-        "ts","dur","pid","display_frame_token","surface_frame_token","layer_name","present_type","on_time_finish","gpu_composition","jank_type","prediction_type","jank_tag","jank_severity_type"
-        20,6,666,2,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        21,16,1000,4,1,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        41,33,1000,6,5,"Layer1","Late Present",0,0,"App Deadline Missed","Valid Prediction","Self Jank","Full"
-        42,5,666,4,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        80,110,1000,17,16,"Layer1","Unknown Present",0,0,"Unknown Jank","Expired Prediction","Self Jank","Partial"
-        81,7,666,6,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        90,16,1000,8,7,"Layer1","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Other Jank","Unknown"
-        108,4,666,8,0,"[NULL]","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Self Jank","Unknown"
-        148,8,666,12,0,"[NULL]","Late Present",0,0,"SurfaceFlinger Scheduling, SurfaceFlinger CPU Deadline Missed","Valid Prediction","Self Jank","Unknown"
-        150,17,1000,15,14,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        150,17,1000,15,14,"Layer2","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        170,6,666,15,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        200,6,666,17,0,"[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
-        245,-1,666,18,0,"[NULL]","Late Present",0,0,"SurfaceFlinger Stuffing","Valid Prediction","SurfaceFlinger Stuffing","Unknown"
-        245,15,666,18,0,"[NULL]","Dropped Frame",0,0,"Dropped Frame","Unspecified Prediction","Dropped Frame","Unknown"
+          "ts","dur","pid","display_frame_token","surface_frame_token","layer_name","present_type","on_time_finish","gpu_composition","jank_type","prediction_type","jank_tag","jank_severity_type"
+          20,6,666,2,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          21,16,1000,4,1,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          41,33,1000,6,5,"Layer1","Late Present",0,0,"App Deadline Missed","Valid Prediction","Self Jank","Full"
+          42,5,666,4,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          80,110,1000,17,16,"Layer1","Unknown Present",0,0,"Unknown Jank","Expired Prediction","Self Jank","Partial"
+          81,7,666,6,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          90,16,1000,8,7,"Layer1","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Other Jank","Unknown"
+          108,4,666,8,"[NULL]","[NULL]","Early Present",1,0,"SurfaceFlinger Scheduling","Valid Prediction","Self Jank","Unknown"
+          148,8,666,12,"[NULL]","[NULL]","Late Present",0,0,"SurfaceFlinger Scheduling, SurfaceFlinger CPU Deadline Missed","Valid Prediction","Self Jank","Unknown"
+          150,17,1000,15,14,"Layer1","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          150,17,1000,15,14,"Layer2","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          170,6,666,15,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          200,6,666,17,"[NULL]","[NULL]","On-time Present",1,0,"None","Valid Prediction","No Jank","None"
+          245,-1,666,18,"[NULL]","[NULL]","Late Present",0,0,"SurfaceFlinger Stuffing","Valid Prediction","SurfaceFlinger Stuffing","Unknown"
+          245,15,666,18,"[NULL]","[NULL]","Dropped Frame",0,0,"Dropped Frame","Unspecified Prediction","Dropped Frame","Unknown"
         """))
 
   # Video 4 Linux 2 related tests
@@ -207,110 +268,6 @@
         1345090746311,1167135,"CTX_DETACH_RESOURCE"
         """))
 
-  # TODO(b/294866695): Reenable
-  # mali GPU events
-  #def test_mali(self):
-  #  return DiffTestBlueprint(
-  #      trace=TextProto(r"""
-  #      packet {
-  #        ftrace_events {
-  #          cpu: 2
-  #          event {
-  #            timestamp: 751796307210
-  #            pid: 2857
-  #            mali_mali_KCPU_CQS_WAIT_START {
-  #              info_val1: 1
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #          event {
-  #            timestamp: 751800621175
-  #            pid: 2857
-  #            mali_mali_KCPU_CQS_WAIT_END {
-  #              info_val1: 412313493488
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #          event {
-  #            timestamp: 751800638997
-  #            pid: 2857
-  #            mali_mali_KCPU_CQS_SET {
-  #              info_val1: 412313493480
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #        }
-  #      }
-  #      """),
-  #      query="""
-  #      SELECT ts, dur, name FROM slice WHERE name GLOB "mali_KCPU_CQS*";
-  #      """,
-  #      out=Csv("""
-  #      "ts","dur","name"
-  #      751796307210,4313965,"mali_KCPU_CQS_WAIT"
-  #      751800638997,0,"mali_KCPU_CQS_SET"
-  #      """))
-
-  #def test_mali_fence(self):
-  #  return DiffTestBlueprint(
-  #      trace=TextProto(r"""
-  #      packet {
-  #        ftrace_events {
-  #          cpu: 2
-  #          event {
-  #            timestamp: 751796307210
-  #            pid: 2857
-  #            mali_mali_KCPU_FENCE_WAIT_START {
-  #              info_val1: 1
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #          event {
-  #            timestamp: 751800621175
-  #            pid: 2857
-  #            mali_mali_KCPU_FENCE_WAIT_END {
-  #              info_val1: 412313493488
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #          event {
-  #            timestamp: 751800638997
-  #            pid: 2857
-  #            mali_mali_KCPU_FENCE_SIGNAL {
-  #              info_val1: 412313493480
-  #              info_val2: 0
-  #              kctx_tgid: 2201
-  #              kctx_id: 10
-  #              id: 0
-  #            }
-  #          }
-  #        }
-  #      }
-  #      """),
-  #      query="""
-  #      SELECT ts, dur, name FROM slice WHERE name GLOB "mali_KCPU_FENCE*";
-  #      """,
-  #      out=Csv("""
-  #      "ts","dur","name"
-  #      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(
@@ -326,4 +283,55 @@
           AND gpu_track.machine_id IS NOT NULL
         ORDER BY ts;
         """,
-        out=Path('graphics_frame_events.out'))
+        out=Csv('''
+          "ts","track_name","dur","slice_name","frame_number","layer_name"
+          1,"Buffer: 1 layer1",0,"Dequeue",11,"layer1"
+          1,"APP_1 layer1",3,"11",11,"layer1"
+          4,"Buffer: 1 layer1",0,"Queue",11,"layer1"
+          4,"GPU_1 layer1",2,"11",11,"layer1"
+          6,"Buffer: 1 layer1",0,"AcquireFenceSignaled",11,"layer1"
+          6,"Buffer: 2 layer2",0,"Dequeue",12,"layer2"
+          6,"APP_2 layer2",3,"12",12,"layer2"
+          7,"Buffer: 7 layer7",0,"unknown_event",15,"layer7"
+          8,"Buffer: 1 layer1",0,"Latch",11,"layer1"
+          8,"Buffer: 2 layer2",0,"AcquireFenceSignaled",12,"layer2"
+          8,"SF_1 layer1",6,"11",11,"layer1"
+          9,"Buffer: 2 layer2",0,"Queue",12,"layer2"
+          11,"Buffer: 2 layer2",0,"Latch",12,"layer2"
+          11,"SF_2 layer2",5,"12",12,"layer2"
+          14,"Buffer: 1 layer1",0,"PresentFenceSignaled",11,"layer1"
+          14,"Display_layer1",10,"11",11,"layer1"
+          16,"Buffer: 2 layer2",0,"PresentFenceSignaled",12,"layer2"
+          16,"Display_layer2",-1,"12",12,"layer2"
+          24,"Buffer: 1 layer1",0,"PresentFenceSignaled",13,"layer1"
+          24,"Display_layer1",-1,"13",13,"layer1"
+          31,"Buffer: 1 layer1",0,"Dequeue",21,"layer1"
+          31,"APP_1 layer1",3,"21",21,"layer1"
+          34,"Buffer: 1 layer1",0,"Queue",21,"layer1"
+          34,"GPU_1 layer1",-1,"21",21,"layer1"
+          37,"Buffer: 1 layer1",0,"Dequeue",22,"layer1"
+          37,"APP_1 layer1",4,"22",22,"layer1"
+          41,"Buffer: 1 layer1",0,"Queue",22,"layer1"
+          41,"GPU_1 layer1",5,"22",22,"layer1"
+          46,"Buffer: 1 layer1",0,"AcquireFenceSignaled",22,"layer1"
+          53,"Buffer: 2 layer2",0,"Dequeue",24,"layer2"
+          53,"APP_2 layer2",-1,"0",0,"layer2"
+          59,"Buffer: 2 layer2",0,"AcquireFenceSignaled",24,"layer2"
+          61,"Buffer: 2 layer2",0,"Latch",24,"layer2"
+          61,"SF_2 layer2",-1,"24",24,"layer2"
+          63,"Buffer: 1 layer1",0,"Dequeue",25,"layer1"
+          63,"APP_1 layer1",-1,"0",0,"layer1"
+          73,"Buffer: 1 layer1",0,"Dequeue",26,"layer1"
+          73,"APP_1 layer1",2,"26",26,"layer1"
+          75,"Buffer: 1 layer1",0,"Queue",26,"layer1"
+          75,"GPU_1 layer1",4,"26",26,"layer1"
+          79,"Buffer: 1 layer1",0,"AcquireFenceSignaled",26,"layer1"
+          81,"Buffer: 1 layer1",0,"Dequeue",30,"layer1"
+          81,"APP_1 layer1",2,"30",30,"layer1"
+          83,"Buffer: 1 layer1",0,"Queue",30,"layer1"
+          83,"GPU_1 layer1",-1,"30",30,"layer1"
+          90,"Buffer: 1 layer2",0,"Dequeue",35,"layer2"
+          90,"APP_1 layer2",2,"35",35,"layer2"
+          92,"Buffer: 1 layer2",0,"Queue",35,"layer2"
+          92,"GPU_1 layer2",-1,"35",35,"layer2"
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
index ead5000..b79e124 100644
--- a/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
+++ b/test/trace_processor/diff_tests/parser/graphics/tests_gpu_trace.py
@@ -65,29 +65,148 @@
   def test_gpu_render_stages(self):
     return DiffTestBlueprint(
         trace=Path('gpu_render_stages.py'),
-        query=Path('gpu_render_stages_test.sql'),
-        out=Path('gpu_render_stages.out'))
+        query='''
+          SELECT
+            g.name AS track_name,
+            g.description AS track_desc,
+            ts,
+            dur,
+            s.name AS slice_name,
+            depth,
+            args.flat_key,
+            args.string_value,
+            s.context_id,
+            render_target,
+            render_target_name,
+            render_pass,
+            render_pass_name,
+            command_buffer,
+            command_buffer_name,
+            submission_id,
+            hw_queue_id,
+            render_subpasses
+          FROM gpu_track g
+          JOIN gpu_slice s ON g.id = s.track_id
+          LEFT JOIN (
+            SELECT arg_set_id, flat_key, string_value
+            FROM args
+            WHERE args.key IS NULL OR args.key NOT IN (
+              'context_id',
+              'render_target',
+              'render_target_name',
+              'render_pass',
+              'render_pass_name',
+              'command_buffer',
+              'command_buffer_name',
+              'submission_id',
+              'hw_queue_id',
+              'render_subpasses',
+              'upid'
+            )
+          ) args USING (arg_set_id)
+          ORDER BY ts;
+        ''',
+        out=Csv('''
+          "track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
+          "queue 1","queue desc 1",0,5,"render stage(1)",0,"[NULL]","[NULL]",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+          "queue 0","queue desc 0",0,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 1","queue desc 1",10,5,"stage 1",0,"description","stage desc 1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+          "queue 2","[NULL]",20,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
+          "queue 0","queue desc 0",30,5,"stage 3",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "Unknown GPU Queue 3","[NULL]",40,5,"render stage(4)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,3,"[NULL]"
+          "queue 0","queue desc 0",50,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",60,5,"stage 0",0,"key1","value1",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",60,5,"stage 0",0,"key2","value2",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",70,5,"stage 0",0,"key1","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",80,5,"stage 2",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",90,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",32,"[NULL]",48,"[NULL]",0,0,"[NULL]"
+          "queue 0","queue desc 0",100,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"[NULL]",16,"command_buffer",0,0,"[NULL]"
+          "queue 0","queue desc 0",110,5,"stage 0",0,"[NULL]","[NULL]",42,16,"[NULL]",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
+          "queue 0","queue desc 0",120,5,"stage 0",0,"[NULL]","[NULL]",42,16,"framebuffer",16,"render_pass",16,"command_buffer",0,0,"[NULL]"
+          "queue 0","queue desc 0",130,5,"stage 0",0,"[NULL]","[NULL]",42,16,"renamed_buffer",0,"[NULL]",0,"[NULL]",0,0,"[NULL]"
+          "Unknown GPU Queue ","[NULL]",140,5,"render stage(18446744073709551615)",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1024,"[NULL]"
+          "queue 0","queue desc 0",150,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"0"
+          "queue 0","queue desc 0",160,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"63,64"
+          "queue 0","queue desc 0",170,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"64"
+          "queue 0","queue desc 0",180,5,"stage 0",0,"[NULL]","[NULL]",42,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,0,"3,68,69,70,71"
+      '''))
 
   def test_gpu_render_stages_interned_spec(self):
     return DiffTestBlueprint(
         trace=Path('gpu_render_stages_interned_spec.textproto'),
-        query=Path('gpu_render_stages_test.sql'),
-        out=Path('gpu_render_stages_interned_spec.out'))
+        query='''
+          SELECT
+            g.name AS track_name,
+            g.description AS track_desc,
+            ts,
+            dur,
+            s.name AS slice_name,
+            depth,
+            args.flat_key,
+            args.string_value,
+            s.context_id,
+            render_target,
+            render_target_name,
+            render_pass,
+            render_pass_name,
+            command_buffer,
+            command_buffer_name,
+            submission_id,
+            hw_queue_id,
+            render_subpasses
+          FROM gpu_track g
+          JOIN gpu_slice s ON g.id = s.track_id
+          LEFT JOIN (
+            SELECT arg_set_id, flat_key, string_value
+            FROM args
+            WHERE args.key IS NULL OR args.key NOT IN (
+              'context_id',
+              'render_target',
+              'render_target_name',
+              'render_pass',
+              'render_pass_name',
+              'command_buffer',
+              'command_buffer_name',
+              'submission_id',
+              'hw_queue_id',
+              'render_subpasses',
+              'upid'
+            )
+          ) args USING (arg_set_id)
+          ORDER BY ts;
+        ''',
+        out=Csv('''
+          "track_name","track_desc","ts","dur","slice_name","depth","flat_key","string_value","context_id","render_target","render_target_name","render_pass","render_pass_name","command_buffer","command_buffer_name","submission_id","hw_queue_id","render_subpasses"
+          "vertex","vertex queue",100,10,"binning",0,"description","binning graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+          "fragment","fragment queue",200,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,2,"[NULL]"
+          "queue2","queue2 description",300,10,"render",0,"description","render graphics",0,0,"[NULL]",0,"[NULL]",0,"[NULL]",0,1,"[NULL]"
+        '''))
 
   def test_vulkan_api_events(self):
     return DiffTestBlueprint(
         trace=Path('vulkan_api_events.py'),
         query="""
-        SELECT track.name AS track_name, gpu_track.description AS track_desc, ts, dur,
-          gpu_slice.name AS slice_name, depth, flat_key, int_value,
-          gpu_slice.context_id, command_buffer, submission_id
-        FROM gpu_track
-        LEFT JOIN track USING (id)
-        JOIN gpu_slice ON gpu_track.id = gpu_slice.track_id
-        LEFT JOIN args ON gpu_slice.arg_set_id = args.arg_set_id
+        SELECT
+          g.name AS track_name,
+          g.description AS track_desc,
+          ts,
+          dur,
+          s.name AS slice_name,
+          depth,
+          s.context_id,
+          command_buffer,
+          submission_id,
+          extract_arg(s.arg_set_id, 'tid') as tid,
+          extract_arg(s.arg_set_id, 'pid') as pid
+        FROM gpu_track g
+        JOIN gpu_slice s ON g.id = s.track_id
         ORDER BY ts;
         """,
-        out=Path('vulkan_api_events.out'))
+        out=Csv('''
+          "track_name","track_desc","ts","dur","slice_name","depth","context_id","command_buffer","submission_id","tid","pid"
+          "Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"[NULL]",100,1,43,42
+          "Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"[NULL]",200,2,45,44
+        '''))
 
   def test_gpu_log(self):
     return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out b/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out
deleted file mode 100644
index de409f9..0000000
--- a/test/trace_processor/diff_tests/parser/graphics/vulkan_api_events.out
+++ /dev/null
@@ -1,5 +0,0 @@
-"track_name","track_desc","ts","dur","slice_name","depth","flat_key","int_value","context_id","command_buffer","submission_id"
-"Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"pid",42,"[NULL]",100,1
-"Vulkan Events","[NULL]",10,2,"vkQueueSubmit",0,"tid",43,"[NULL]",100,1
-"Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"pid",44,"[NULL]",200,2
-"Vulkan Events","[NULL]",20,2,"vkQueueSubmit",0,"tid",45,"[NULL]",200,2
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index dba762d..25017ca 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -618,6 +618,102 @@
         101000004,"test2","producer2",4
         """))
 
+  def test_triggers_packets_clone_snapshot_trigger_packet(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+      packet {
+        clone_snapshot_trigger {
+          trigger_name: "test1"
+          trusted_producer_uid: 3
+          producer_name: "producer1"
+        }
+        timestamp: 101000002
+      }
+      """),
+        query=Path('triggers_packets_test.sql'),
+        out=Csv("""
+      "ts","name","string_value","int_value"
+      101000002,"test1","producer1",3
+      """))
+
+  def test_trigger_metadata_test_clone_snapshot_trigger_packet(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+    packet {
+      clone_snapshot_trigger {
+        trigger_name: "test1"
+        trusted_producer_uid: 3
+        producer_name: "producer1"
+      }
+      timestamp: 101000002
+    }
+    """),
+        query=Path('trigger_metadata_test.sql'),
+        out=Csv("""
+      "str_value"
+      "test1"
+      """))
+
+  def test_trigger_metadata_trigger_packet(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          trigger {
+            trigger_name: "test1"
+            trusted_producer_uid: 1
+            producer_name: "producer1"
+          }
+          timestamp: 101000002
+        }
+        packet {
+          trigger {
+            trigger_name: "test2"
+            trusted_producer_uid: 2
+            producer_name: "producer2"
+          }
+          timestamp: 101000001
+        }
+        """),
+        query=Path('trigger_metadata_test.sql'),
+        out=Csv("""
+      "str_value"
+      "test2"
+      """))
+
+  def test_trigger_metadata_trigger_packet_and_clone_snapshot_packet(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+      packet {
+        trigger {
+          trigger_name: "test1"
+          trusted_producer_uid: 1
+          producer_name: "producer1"
+        }
+        timestamp: 101000004
+      }
+      packet {
+        trigger {
+          trigger_name: "test2"
+          trusted_producer_uid: 2
+          producer_name: "producer2"
+        }
+        timestamp: 101000002
+      }
+      packet {
+        clone_snapshot_trigger {
+          trigger_name: "testClone"
+          trusted_producer_uid: 3
+          producer_name: "producer3"
+        }
+        timestamp: 101000003
+      }
+      """),
+        query=Path('trigger_metadata_test.sql'),
+        out=Csv("""
+    "str_value"
+    "testClone"
+    """))
+
   def test_chrome_metadata(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
diff --git a/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql b/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql
new file mode 100644
index 0000000..45f85e8
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/trigger_metadata_test.sql
@@ -0,0 +1,16 @@
+--
+-- Copyright 2019 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+SELECT str_value FROM metadata WHERE name = 'trace_trigger';
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 419e171..fef7df6 100644
--- a/test/trace_processor/diff_tests/parser/track_event/tests.py
+++ b/test/trace_processor/diff_tests/parser/track_event/tests.py
@@ -300,13 +300,13 @@
         "async2","process=p1",1
         "async3","thread=t2",1
         "event_and_track_async3","process=p1",1
-        "process=p1","[NULL]","[NULL]"
+        "process=p1","[NULL]",1
         "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]"
+        "thread=t1","[NULL]",1
+        "thread=t2","[NULL]",1
+        "thread=t3","[NULL]",1
+        "thread=t4","[NULL]","[NULL]"
         "tid=1","[NULL]","[NULL]"
         """))
 
@@ -532,13 +532,10 @@
           "event.category","event.category","[NULL]","cat"
           "event.name","event.name","[NULL]","[NULL]"
           "event.name","event.name","[NULL]","name1"
-          "is_root_in_scope","is_root_in_scope",1,"[NULL]"
           "legacy_event.passthrough_utid","legacy_event.passthrough_utid",1,"[NULL]"
           "scope","scope","[NULL]","cat"
           "source","source","[NULL]","chrome"
-          "source","source","[NULL]","descriptor"
           "source_scope","source_scope","[NULL]","cat"
-          "trace_id","trace_id",1,"[NULL]"
           "trace_id","trace_id",1234,"[NULL]"
           "trace_id_is_process_scoped","trace_id_is_process_scoped",0,"[NULL]"
           "upid","upid",1,"[NULL]"
@@ -856,10 +853,6 @@
         6,0,"[NULL]","[NULL]"
         7,"[NULL]","[NULL]","[NULL]"
         8,7,"[NULL]","[NULL]"
-        9,"[NULL]","[NULL]","[NULL]"
-        10,"[NULL]","[NULL]","[NULL]"
-        11,"[NULL]","[NULL]","[NULL]"
-        12,0,"[NULL]","[NULL]"
         """))
 
   def test_track_event_tracks_machine_id(self):
@@ -900,13 +893,13 @@
         "async2","process=p1",1
         "async3","thread=t2",1
         "event_and_track_async3","process=p1",1
-        "process=p1","[NULL]","[NULL]"
+        "process=p1","[NULL]",1
         "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]"
+        "thread=t1","[NULL]",1
+        "thread=t2","[NULL]",1
+        "thread=t3","[NULL]",1
+        "thread=t4","[NULL]","[NULL]"
         "tid=1","[NULL]","[NULL]"
         """))
 
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
index 779998b..7de8dcc 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
@@ -6,12 +6,12 @@
   first_packet_on_sequence: true
   track_descriptor {
     uuid: 1
-    parent_uuid: 10
     thread {
       pid: 5
       tid: 1
       thread_name: "t1"
     }
+    disallow_merging_with_system_tracks: true
   }
   trace_packet_defaults {
     track_event_defaults {
@@ -27,12 +27,12 @@
   first_packet_on_sequence: true
   track_descriptor {
     uuid: 2
-    parent_uuid: 10
     thread {
       pid: 5
       tid: 2
       thread_name: "t2"
     }
+    disallow_merging_with_system_tracks: true
   }
   trace_packet_defaults {
     track_event_defaults {
@@ -179,12 +179,12 @@
   first_packet_on_sequence: true
   track_descriptor {
     uuid: 3
-    parent_uuid: 10
     thread {
       pid: 5
       tid: 1
       thread_name: "t3"
     }
+    disallow_merging_with_system_tracks: true
   }
 }
 # Should appear on t3.
@@ -231,7 +231,6 @@
   incremental_state_cleared: true
   track_descriptor {
     uuid: 21
-    parent_uuid: 20
     thread {
       pid: 5
       tid: 4
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
index 892266b..460367a 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_ordering.textproto
@@ -7,11 +7,6 @@
   track_descriptor {
     uuid: 1
     parent_uuid: 10
-    thread {
-      pid: 5
-      tid: 1
-      thread_name: "t1"
-    }
     sibling_order_rank: -10
   }
   trace_packet_defaults {
@@ -29,11 +24,6 @@
   track_descriptor {
     uuid: 2
     parent_uuid: 10
-    thread {
-      pid: 5
-      tid: 2
-      thread_name: "t2"
-    }
     sibling_order_rank: -2
   }
   trace_packet_defaults {
@@ -99,82 +89,6 @@
     name: "async3"
   }
 }
-
-# Should appear on default track "t1".
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 1000
-  track_event {
-    categories: "cat"
-    name: "event1_on_t1"
-    type: 3
-  }
-}
-# Should appear on default track "t2".
-packet {
-  trusted_packet_sequence_id: 2
-  timestamp: 2000
-  track_event {
-    categories: "cat"
-    name: "event1_on_t2"
-    type: 3
-  }
-}
-# Should appear on overridden track "t2".
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 3000
-  track_event {
-    track_uuid: 2
-    categories: "cat"
-    name: "event2_on_t2"
-    type: 3
-  }
-}
-# Should appear on process track.
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 4000
-  track_event {
-    track_uuid: 10
-    categories: "cat"
-    name: "event1_on_p1"
-    type: 3
-  }
-}
-# Should appear on async track.
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 5000
-  track_event {
-    track_uuid: 11
-    categories: "cat"
-    name: "event1_on_async"
-    type: 3
-  }
-}
-# Event for the "async2" track starting on one thread and ending on another.
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 5100
-  track_event {
-    track_uuid: 12
-    categories: "cat"
-    name: "event1_on_async2"
-    type: 1
-  }
-}
-packet {
-  trusted_packet_sequence_id: 2
-  timestamp: 5200
-  track_event {
-    track_uuid: 12
-    categories: "cat"
-    name: "event1_on_async2"
-    type: 2
-  }
-}
-
 # If we later see another track descriptor for tid 1, but with a different uuid,
 # we should detect tid reuse and start a new thread.
 packet {
@@ -185,11 +99,6 @@
   track_descriptor {
     uuid: 3
     parent_uuid: 10
-    thread {
-      pid: 5
-      tid: 1
-      thread_name: "t3"
-    }
   }
 }
 # Should appear on t3.
@@ -203,7 +112,6 @@
     type: 3
   }
 }
-
 # If we later see another track descriptor for pid 5, but with a different uuid,
 # we should detect pid reuse and start a new process.
 packet {
@@ -218,18 +126,6 @@
     }
   }
 }
-# Should appear on p2.
-packet {
-  trusted_packet_sequence_id: 4
-  timestamp: 21000
-  track_event {
-    track_uuid: 20
-    categories: "cat"
-    name: "event1_on_p2"
-    type: 3
-  }
-}
-# Another thread t4 in the new process.
 packet {
   trusted_packet_sequence_id: 4
   timestamp: 22000
@@ -237,102 +133,5 @@
   track_descriptor {
     uuid: 21
     parent_uuid: 20
-    thread {
-      pid: 5
-      tid: 4
-      thread_name: "t4"
-    }
-  }
-}
-# Should appear on t4.
-packet {
-  trusted_packet_sequence_id: 4
-  timestamp: 22000
-  track_event {
-    track_uuid: 21
-    categories: "cat"
-    name: "event1_on_t4"
-    type: 3
-  }
-}
-
-# Another packet for a thread track in the old process, badly sorted.
-packet {
-  trusted_packet_sequence_id: 2
-  timestamp: 6000
-  track_event {
-    track_uuid: 1
-    categories: "cat"
-    name: "event3_on_t1"
-    type: 3
-  }
-}
-
-# Override the track to the default descriptor track for an event with a
-# TrackEvent type. Should appear on the default descriptor track instead of
-# "t1".
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 30000
-  track_event {
-    track_uuid: 0
-    categories: "cat"
-    name: "event1_on_t1"
-    type: 3
-  }
-}
-
-# But a legacy event without TrackEvent type falls back to legacy tracks (based
-# on ThreadDescriptor / async IDs / legacy instant scopes). This instant event
-# should appear on the process track "p2".
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 31000
-  track_event {
-    track_uuid: 0
-    categories: "cat"
-    name: "event2_on_p2"
-    legacy_event {
-      phase: 73               # 'I'
-      instant_event_scope: 2  # Process scope
-    }
-  }
-}
-
-# And pid/tid overrides take effect even for TrackEvent type events.
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 32000
-  track_event {
-    track_uuid: 0
-    categories: "cat"
-    name: "event2_on_t4"
-    type: 3
-    legacy_event {
-      pid_override: 5
-      tid_override: 4
-    }
-  }
-}
-
-# Track descriptor without name and process/thread association derives its
-# name from the first event on the track.
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 40000
-  track_descriptor {
-    uuid: 13
-    parent_uuid: 10
-  }
-}
-
-packet {
-  trusted_packet_sequence_id: 1
-  timestamp: 40000
-  track_event {
-    track_uuid: 13
-    categories: "cat"
-    name: "event_and_track_async3"
-    type: 3
   }
 }
diff --git a/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
index 2e88593..75ed889 100644
--- a/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
+++ b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
@@ -26,16 +26,13 @@
 "event.name","event.name","[NULL]","name6"
 "int_extension_for_testing","int_extension_for_testing[0]",42,"[NULL]"
 "int_extension_for_testing","int_extension_for_testing[1]",1337,"[NULL]"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
 "nested_message_extension_for_testing.arg1","nested_message_extension_for_testing.arg1","[NULL]","value"
 "nested_message_extension_for_testing.arg2.key","nested_message_extension_for_testing.arg2.key","[NULL]","value"
 "nested_message_extension_for_testing.child_field_for_testing","nested_message_extension_for_testing.child_field_for_testing","[NULL]","nesting test"
-"source","source","[NULL]","descriptor"
 "source.file_name","source.file_name","[NULL]","source.cc"
 "source.function_name","source.function_name","[NULL]","SourceFunction"
 "source.line_number","source.line_number",0,"[NULL]"
 "source_location_iid","source_location_iid",1,"[NULL]"
 "string_extension_for_testing","string_extension_for_testing","[NULL]","an extension string!"
 "string_extension_for_testing2","string_extension_for_testing2","[NULL]","a second extension string!"
-"trace_id","trace_id",1,"[NULL]"
 "utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
index efbb466..cacf4eb 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
@@ -11,7 +11,4 @@
 "event.name","event.name","[NULL]","slice1"
 "event.name","event.name","[NULL]","slice2"
 "event.name","event.name","[NULL]","slice3"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
 "utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
index fd17777ee..1d27656 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
@@ -5,7 +5,4 @@
 "chrome_hashed_performance_mark.site_hash","chrome_hashed_performance_mark.site_hash",10,"[NULL]"
 "event.category","event.category","[NULL]","cat1"
 "event.name","event.name","[NULL]","slice1"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
 "utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
index ace9de6..7ea9784 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
@@ -10,7 +10,4 @@
 "event.name","event.name","[NULL]","slice1"
 "event.name","event.name","[NULL]","slice2"
 "event.name","event.name","[NULL]","slice3"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
 "utid","utid",1,"[NULL]"
diff --git a/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
index ea05157..9fac5d8 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
+++ b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
@@ -15,7 +15,4 @@
 "event.name","event.name","[NULL]","slice2"
 "event.name","event.name","[NULL]","slice3"
 "event.name","event.name","[NULL]","slice4"
-"is_root_in_scope","is_root_in_scope",1,"[NULL]"
-"source","source","[NULL]","descriptor"
-"trace_id","trace_id",12345,"[NULL]"
 "utid","utid",1,"[NULL]"
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 64ae4d59..2411809 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
@@ -597,28 +597,28 @@
         """,
         out=Csv("""
         "id","presented_in_frame_id","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","generation_to_browser_main_dur","browser_utid","touch_move_received_slice_id","touch_move_received_ts","touch_move_processing_dur","scroll_update_created_slice_id","scroll_update_created_ts","scroll_update_processing_dur","scroll_update_created_end_ts","browser_to_compositor_delay_dur","compositor_utid","compositor_dispatch_slice_id","compositor_dispatch_ts","compositor_dispatch_dur","compositor_dispatch_end_ts","compositor_dispatch_to_coalesced_input_handled_dur","compositor_coalesced_input_handled_slice_id","compositor_coalesced_input_handled_ts","compositor_coalesced_input_handled_dur","compositor_coalesced_input_handled_end_ts"
-        -2143831735395280256,-2143831735395280256,1,0,1,0,1,1292554141489270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10781,1292554142167257,363000,1292554142530257,472953,4,10796,1292554143003210,108000,1292554143111210,10912000,10827,1292554154023210,83000,1292554154106210
-        -2143831735395280254,-2143831735395280254,1,0,1,0,1,1292554152575270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10830,1292554154230257,259000,1292554154489257,698953,4,10845,1292554155188210,120000,1292554155308210,9637000,10869,1292554164945210,223000,1292554165168210
-        -2143831735395280250,-2143831735395280250,1,0,1,0,1,1292554130385270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10742,1292554131192257,279000,1292554131471257,393953,4,10757,1292554131865210,98000,1292554131963210,10636000,10790,1292554142599210,191000,1292554142790210
-        -2143831735395280248,-2143831735395280248,1,0,1,0,1,1292554185877270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10939,1292554186628257,398000,1292554187026257,217953,4,10950,1292554187244210,107000,1292554187351210,10849000,10988,1292554198200210,82000,1292554198282210
-        -2143831735395280246,-2143831735395280246,1,0,1,0,1,1292554196968270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10980,1292554198042257,362000,1292554198404257,890953,4,11000,1292554199295210,110000,1292554199405210,9963000,11025,1292554209368210,90000,1292554209458210
-        -2143831735395280244,-2143831735395280244,1,0,1,0,1,1292554163682270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10860,1292554164468257,393000,1292554164861257,513953,4,10876,1292554165375210,127000,1292554165502210,10798000,10908,1292554176300210,226000,1292554176526210
-        -2143831735395280242,-2143831735395280242,1,0,1,0,1,1292554174786270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10899,1292554175708257,321000,1292554176029257,697953,4,10915,1292554176727210,107000,1292554176834210,10177000,10947,1292554187011210,88000,1292554187099210
-        -2143831735395280239,-2143831735395280239,1,0,1,0,1,1292554086893270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10555,1292554086897257,128000,1292554087025257,1290953,4,10586,1292554088316210,79000,1292554088395210,9853000,10620,1292554098248210,177000,1292554098425210
-        -2143831735395280229,-2143831735395280229,1,0,1,0,1,1292554119302270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10699,1292554120042257,327000,1292554120369257,167953,4,10714,1292554120537210,94000,1292554120631210,10935000,10750,1292554131566210,158000,1292554131724210
-        -2143831735395280227,-2143831735395280227,1,0,1,0,1,1292554097138270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10611,1292554097987257,189000,1292554098176257,366953,4,10626,1292554098543210,76000,1292554098619210,10479000,10662,1292554109098210,151000,1292554109249210
-        -2143831735395280226,-2143831735395280226,1,0,1,0,1,1292554108216270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",10657,1292554108988257,322000,1292554109310257,80953,4,10666,1292554109391210,100000,1292554109491210,10760000,10706,1292554120251210,138000,1292554120389210
-        -2143831735395280208,-2143831735395280208,1,0,1,0,1,1292554230251270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11096,1292554231054257,408000,1292554231462257,145953,4,11106,1292554231608210,103000,1292554231711210,11015000,11142,1292554242726210,128000,1292554242854210
-        -2143831735395280206,-2143831735395280206,1,0,1,0,1,1292554241443270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11134,1292554242336257,324000,1292554242660257,335953,4,11148,1292554242996210,120000,1292554243116210,11070000,11184,1292554254186210,138000,1292554254324210
-        -2143831735395280204,-2143831735395280204,1,0,1,0,1,1292554208072270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11017,1292554208931257,257000,1292554209188257,423953,4,11031,1292554209612210,110000,1292554209722210,10857000,11064,1292554220579210,107000,1292554220686210
-        -2143831735395280202,-2143831735395280202,1,0,1,0,1,1292554219159270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11057,1292554220303257,375000,1292554220678257,1225953,4,11078,1292554221904210,150000,1292554222054210,9337000,11103,1292554231391210,77000,1292554231468210
-        -2143831735395280200,-2143831735395280200,0,0,1,0,1,1292554274773270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11250,1292554275745257,304000,1292554276049257,837953,4,11266,1292554276887210,144000,1292554277031210,9856000,11290,1292554286887210,242000,1292554287129210
-        -2143831735395280196,-2143831735395280196,1,0,1,0,1,1292554252553270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11172,1292554253301257,345000,1292554253646257,819953,4,11187,1292554254466210,119000,1292554254585210,11932000,11223,1292554266517210,117000,1292554266634210
-        -2143831735395280194,-2143831735395280194,0,0,1,0,1,1292554263653270,"[NULL]",1,"[NULL]","[NULL]","[NULL]",11211,1292554264600257,279000,1292554264879257,1915953,4,11227,1292554266795210,193000,1292554266988210,9556000,11259,1292554276544210,133000,1292554276677210
+        -2143831735395280256,-2143831735395280256,1,0,1,0,1,1292554141489270,677987,1,"[NULL]","[NULL]","[NULL]",10781,1292554142167257,363000,1292554142530257,472953,4,10796,1292554143003210,108000,1292554143111210,10912000,10827,1292554154023210,83000,1292554154106210
+        -2143831735395280254,-2143831735395280254,1,0,1,0,1,1292554152575270,1654987,1,"[NULL]","[NULL]","[NULL]",10830,1292554154230257,259000,1292554154489257,698953,4,10845,1292554155188210,120000,1292554155308210,9637000,10869,1292554164945210,223000,1292554165168210
+        -2143831735395280250,-2143831735395280250,1,0,1,0,1,1292554130385270,806987,1,"[NULL]","[NULL]","[NULL]",10742,1292554131192257,279000,1292554131471257,393953,4,10757,1292554131865210,98000,1292554131963210,10636000,10790,1292554142599210,191000,1292554142790210
+        -2143831735395280248,-2143831735395280248,1,0,1,0,1,1292554185877270,750987,1,"[NULL]","[NULL]","[NULL]",10939,1292554186628257,398000,1292554187026257,217953,4,10950,1292554187244210,107000,1292554187351210,10849000,10988,1292554198200210,82000,1292554198282210
+        -2143831735395280246,-2143831735395280246,1,0,1,0,1,1292554196968270,1073987,1,"[NULL]","[NULL]","[NULL]",10980,1292554198042257,362000,1292554198404257,890953,4,11000,1292554199295210,110000,1292554199405210,9963000,11025,1292554209368210,90000,1292554209458210
+        -2143831735395280244,-2143831735395280244,1,0,1,0,1,1292554163682270,785987,1,"[NULL]","[NULL]","[NULL]",10860,1292554164468257,393000,1292554164861257,513953,4,10876,1292554165375210,127000,1292554165502210,10798000,10908,1292554176300210,226000,1292554176526210
+        -2143831735395280242,-2143831735395280242,1,0,1,0,1,1292554174786270,921987,1,"[NULL]","[NULL]","[NULL]",10899,1292554175708257,321000,1292554176029257,697953,4,10915,1292554176727210,107000,1292554176834210,10177000,10947,1292554187011210,88000,1292554187099210
+        -2143831735395280239,-2143831735395280239,1,0,1,0,1,1292554086893270,3987,1,"[NULL]","[NULL]","[NULL]",10555,1292554086897257,128000,1292554087025257,1290953,4,10586,1292554088316210,79000,1292554088395210,9853000,10620,1292554098248210,177000,1292554098425210
+        -2143831735395280229,-2143831735395280229,1,0,1,0,1,1292554119302270,739987,1,"[NULL]","[NULL]","[NULL]",10699,1292554120042257,327000,1292554120369257,167953,4,10714,1292554120537210,94000,1292554120631210,10935000,10750,1292554131566210,158000,1292554131724210
+        -2143831735395280227,-2143831735395280227,1,0,1,0,1,1292554097138270,848987,1,"[NULL]","[NULL]","[NULL]",10611,1292554097987257,189000,1292554098176257,366953,4,10626,1292554098543210,76000,1292554098619210,10479000,10662,1292554109098210,151000,1292554109249210
+        -2143831735395280226,-2143831735395280226,1,0,1,0,1,1292554108216270,771987,1,"[NULL]","[NULL]","[NULL]",10657,1292554108988257,322000,1292554109310257,80953,4,10666,1292554109391210,100000,1292554109491210,10760000,10706,1292554120251210,138000,1292554120389210
+        -2143831735395280208,-2143831735395280208,1,0,1,0,1,1292554230251270,802987,1,"[NULL]","[NULL]","[NULL]",11096,1292554231054257,408000,1292554231462257,145953,4,11106,1292554231608210,103000,1292554231711210,11015000,11142,1292554242726210,128000,1292554242854210
+        -2143831735395280206,-2143831735395280206,1,0,1,0,1,1292554241443270,892987,1,"[NULL]","[NULL]","[NULL]",11134,1292554242336257,324000,1292554242660257,335953,4,11148,1292554242996210,120000,1292554243116210,11070000,11184,1292554254186210,138000,1292554254324210
+        -2143831735395280204,-2143831735395280204,1,0,1,0,1,1292554208072270,858987,1,"[NULL]","[NULL]","[NULL]",11017,1292554208931257,257000,1292554209188257,423953,4,11031,1292554209612210,110000,1292554209722210,10857000,11064,1292554220579210,107000,1292554220686210
+        -2143831735395280202,-2143831735395280202,1,0,1,0,1,1292554219159270,1143987,1,"[NULL]","[NULL]","[NULL]",11057,1292554220303257,375000,1292554220678257,1225953,4,11078,1292554221904210,150000,1292554222054210,9337000,11103,1292554231391210,77000,1292554231468210
+        -2143831735395280200,-2143831735395280200,0,0,1,0,1,1292554274773270,971987,1,"[NULL]","[NULL]","[NULL]",11250,1292554275745257,304000,1292554276049257,837953,4,11266,1292554276887210,144000,1292554277031210,9856000,11290,1292554286887210,242000,1292554287129210
+        -2143831735395280196,-2143831735395280196,1,0,1,0,1,1292554252553270,747987,1,"[NULL]","[NULL]","[NULL]",11172,1292554253301257,345000,1292554253646257,819953,4,11187,1292554254466210,119000,1292554254585210,11932000,11223,1292554266517210,117000,1292554266634210
+        -2143831735395280194,-2143831735395280194,0,0,1,0,1,1292554263653270,946987,1,"[NULL]","[NULL]","[NULL]",11211,1292554264600257,279000,1292554264879257,1915953,4,11227,1292554266795210,193000,1292554266988210,9556000,11259,1292554276544210,133000,1292554276677210
         -2143831735395280183,-2143831735395280179,0,0,0,0,0,1292554034979270,3955987,1,10192,1292554038935257,286000,10197,1292554039221257,141000,1292554039362257,17953,4,10210,1292554039380210,124000,1292554039504210,3940000,10230,1292554043444210,101000,1292554043545210
         -2143831735395280179,-2143831735395280179,1,0,0,0,1,1292554029441270,7839987,1,10172,1292554037281257,337000,10177,1292554037618257,167000,1292554037785257,451953,4,10189,1292554038237210,89000,1292554038326210,4800000,10229,1292554043126210,303000,1292554043429210
         -2143831735395280166,-2143831735395280166,1,0,0,1,1,1292554023976270,3704987,1,10071,1292554027681257,2166000,10102,1292554029847257,236000,1292554030083257,276953,4,10123,1292554030360210,377000,1292554030737210,-68000,10128,1292554030669210,56000,1292554030725210
-  """))
+        """))
 
   def test_chrome_scroll_update_frame_info(self):
         return DiffTestBlueprint(
@@ -658,38 +658,36 @@
           viz_swap_buffers_end_ts,
           viz_swap_buffers_to_latch_dur,
           latch_timestamp,
-          viz_latch_to_swap_end_dur,
-          swap_end_timestamp,
-          swap_end_to_presentation_dur,
+          viz_latch_to_presentation_dur,
           presentation_timestamp
         FROM chrome_scroll_update_frame_info
         ORDER BY id
         LIMIT 21
         """,
         out=Csv("""
-        "id","vsync_interval_ms","compositor_resample_slice_id","compositor_resample_ts","compositor_generate_compositor_frame_slice_id","compositor_generate_compositor_frame_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_compositor_frame_slice_id","compositor_submit_compositor_frame_ts","compositor_submit_frame_dur","compositor_submit_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_compositor_utid","viz_receive_compositor_frame_slice_id","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_dur","viz_receive_compositor_frame_end_ts","viz_wait_for_draw_dur","viz_draw_and_swap_slice_id","viz_draw_and_swap_ts","viz_draw_and_swap_dur","viz_send_buffer_swap_slice_id","viz_send_buffer_swap_end_ts","viz_to_gpu_delay_dur","viz_gpu_thread_utid","viz_swap_buffers_slice_id","viz_swap_buffers_ts","viz_swap_buffers_dur","viz_swap_buffers_end_ts","viz_swap_buffers_to_latch_dur","latch_timestamp","viz_latch_to_swap_end_dur","swap_end_timestamp","swap_end_to_presentation_dur","presentation_timestamp"
-        -2143831735395280256,11.111000,"[NULL]","[NULL]",10834,1292554154282210,337000,10838,1292554154619210,337000,1292554154956210,139423,6,10840,1292554155095633,126000,1292554155221633,65000,10846,1292554155286633,1295000,10849,1292554156581633,1620498,7,10850,1292554158202131,536000,1292554158738131,10898139,1292554169636270,6818000,1292554176454270,9345000,1292554185799270
-        -2143831735395280254,11.111000,"[NULL]","[NULL]",10878,1292554165562210,387000,10881,1292554165949210,363000,1292554166312210,148423,6,10882,1292554166460633,129000,1292554166589633,101000,10885,1292554166690633,1134000,10888,1292554167824633,1573498,7,10889,1292554169398131,545000,1292554169943131,10941139,1292554180884270,6702000,1292554187586270,9405000,1292554196991270
-        -2143831735395280250,11.111000,"[NULL]","[NULL]",10799,1292554143168210,382000,10802,1292554143550210,355000,1292554143905210,122423,6,10803,1292554144027633,99000,1292554144126633,46000,10806,1292554144172633,1016000,10809,1292554145188633,1514498,7,10810,1292554146703131,483000,1292554147186131,11323139,1292554158509270,6698000,1292554165207270,9484000,1292554174691270
-        -2143831735395280248,11.111000,"[NULL]","[NULL]",10991,1292554198448210,361000,10995,1292554198809210,317000,1292554199126210,167423,6,10996,1292554199293633,123000,1292554199416633,66000,11002,1292554199482633,1058000,11005,1292554200540633,1691498,7,11006,1292554202232131,543000,1292554202775131,11459139,1292554214234270,6958000,1292554221192270,9043000,1292554230235270
-        -2143831735395280246,11.111000,"[NULL]","[NULL]",11033,1292554209783210,326000,11037,1292554210109210,338000,1292554210447210,139423,6,11038,1292554210586633,158000,1292554210744633,61000,11041,1292554210805633,1109000,11044,1292554211914633,763498,7,11045,1292554212678131,458000,1292554213136131,12006139,1292554225142270,6727000,1292554231869270,9462000,1292554241331270
-        -2143831735395280244,11.111000,"[NULL]","[NULL]",10917,1292554176906210,371000,10920,1292554177277210,302000,1292554177579210,135423,6,10921,1292554177714633,105000,1292554177819633,47000,10924,1292554177866633,1033000,10927,1292554178899633,1364498,7,10928,1292554180264131,468000,1292554180732131,11126139,1292554191858270,6255000,1292554198113270,9900000,1292554208013270
-        -2143831735395280242,11.111000,"[NULL]","[NULL]",10953,1292554187399210,383000,10959,1292554187782210,302000,1292554188084210,153423,6,10960,1292554188237633,149000,1292554188386633,57000,10963,1292554188443633,1080000,10966,1292554189523633,1628498,7,10967,1292554191152131,537000,1292554191689131,11488139,1292554203177270,6351000,1292554209528270,9588000,1292554219116270
-        -2143831735395280239,11.111000,10616,1292554097735210,10629,1292554098654210,282000,10632,1292554098936210,237000,1292554099173210,121423,6,10633,1292554099294633,113000,1292554099407633,62000,10636,1292554099469633,953000,10641,1292554100422633,1211498,7,10643,1292554101634131,364000,1292554101998131,12589139,1292554114587270,5702000,1292554120289270,10025000,1292554130314270
-        -2143831735395280229,11.111000,"[NULL]","[NULL]",10759,1292554132003210,421000,10762,1292554132424210,509000,1292554132933210,113423,6,10763,1292554133046633,120000,1292554133166633,96000,10766,1292554133262633,1095000,10769,1292554134357633,1469498,7,10770,1292554135827131,499000,1292554136326131,11256139,1292554147582270,6606000,1292554154188270,9466000,1292554163654270
-        -2143831735395280227,11.111000,"[NULL]","[NULL]",10669,1292554109537210,306000,10677,1292554109843210,243000,1292554110086210,214423,6,10679,1292554110300633,119000,1292554110419633,42000,10682,1292554110461633,953000,10685,1292554111414633,731498,7,10686,1292554112146131,440000,1292554112586131,12791139,1292554125377270,6239000,1292554131616270,9725000,1292554141341270
-        -2143831735395280226,11.111000,"[NULL]","[NULL]",10716,1292554120708210,290000,10721,1292554120998210,195000,1292554121193210,190423,6,10724,1292554121383633,89000,1292554121472633,42000,10727,1292554121514633,923000,10730,1292554122437633,1542498,7,10731,1292554123980131,426000,1292554124406131,12351139,1292554136757270,6289000,1292554143046270,9504000,1292554152550270
-        -2143831735395280208,11.111000,"[NULL]","[NULL]",11150,1292554243173210,360000,11153,1292554243533210,219000,1292554243752210,267423,6,11154,1292554244019633,158000,1292554244177633,61000,11157,1292554244238633,1109000,11161,1292554245347633,1467498,7,11162,1292554246815131,558000,1292554247373131,11123139,1292554258496270,6595000,1292554265091270,9589000,1292554274680270
-        -2143831735395280206,11.111000,"[NULL]","[NULL]",11189,1292554254651210,430000,11192,1292554255081210,201000,1292554255282210,299423,6,11193,1292554255581633,129000,1292554255710633,66000,11197,1292554255776633,1110000,11200,1292554256886633,1522498,7,11201,1292554258409131,591000,1292554259000131,10967139,1292554269967270,6691000,1292554276658270,9116000,1292554285774270
-        -2143831735395280204,11.111000,"[NULL]","[NULL]",11066,1292554220890210,434000,11073,1292554221324210,355000,1292554221679210,197423,6,11074,1292554221876633,144000,1292554222020633,67000,11080,1292554222087633,1470000,11083,1292554223557633,1731498,7,11085,1292554225289131,658000,1292554225947131,10096139,1292554236043270,6959000,1292554243002270,9447000,1292554252449270
-        -2143831735395280202,11.111000,"[NULL]","[NULL]",11109,1292554231758210,340000,11115,1292554232098210,296000,1292554232394210,127423,6,11116,1292554232521633,154000,1292554232675633,62000,11119,1292554232737633,1042000,11122,1292554233779633,1446498,7,11123,1292554235226131,479000,1292554235705131,11595139,1292554247300270,6465000,1292554253765270,9784000,1292554263549270
-        -2143831735395280200,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
-        -2143831735395280196,11.111000,"[NULL]","[NULL]",11229,1292554267072210,466000,11232,1292554267538210,202000,1292554267740210,469423,6,11233,1292554268209633,196000,1292554268405633,61000,11236,1292554268466633,1147000,11239,1292554269613633,2276498,7,11241,1292554271890131,695000,1292554272585131,8276139,1292554280861270,6143000,1292554287004270,9906000,1292554296910270
-        -2143831735395280194,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
-        -2143831735395280179,11.111000,10223,1292554042749210,10233,1292554043721210,451000,10239,1292554044172210,315000,1292554044487210,169423,6,10245,1292554044656633,670000,1292554045326633,1785000,10266,1292554047111633,1048000,10271,1292554048159633,1540498,7,10272,1292554049700131,458000,1292554050158131,8651139,1292554058809270,6861000,1292554065670270,9035000,1292554074705270
-        -2143831735395280166,11.111000,10124,1292554030360210,10130,1292554030873210,568000,10135,1292554031441210,227000,1292554031668210,919423,6,10141,1292554032587633,754000,1292554033341633,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,5822000,1292554053830270,9774000,1292554063604270
-        -2143831735395280153,11.111000,10469,1292554075561210,10481,1292554076592210,334000,10488,1292554076926210,301000,1292554077227210,177423,6,10494,1292554077404633,138000,1292554077542633,280000,10506,1292554077822633,988000,10509,1292554078810633,1377498,7,10516,1292554080188131,494000,1292554080682131,11265139,1292554091947270,7535000,1292554099482270,8561000,1292554108043270
-  """))
+        "id","vsync_interval_ms","compositor_resample_slice_id","compositor_resample_ts","compositor_generate_compositor_frame_slice_id","compositor_generate_compositor_frame_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_compositor_frame_slice_id","compositor_submit_compositor_frame_ts","compositor_submit_frame_dur","compositor_submit_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_compositor_utid","viz_receive_compositor_frame_slice_id","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_dur","viz_receive_compositor_frame_end_ts","viz_wait_for_draw_dur","viz_draw_and_swap_slice_id","viz_draw_and_swap_ts","viz_draw_and_swap_dur","viz_send_buffer_swap_slice_id","viz_send_buffer_swap_end_ts","viz_to_gpu_delay_dur","viz_gpu_thread_utid","viz_swap_buffers_slice_id","viz_swap_buffers_ts","viz_swap_buffers_dur","viz_swap_buffers_end_ts","viz_swap_buffers_to_latch_dur","latch_timestamp","viz_latch_to_presentation_dur","presentation_timestamp"
+        -2143831735395280256,11.111000,"[NULL]","[NULL]",10834,1292554154282210,337000,10838,1292554154619210,337000,1292554154956210,139423,6,10840,1292554155095633,126000,1292554155221633,65000,10846,1292554155286633,1295000,10849,1292554156581633,1620498,7,10850,1292554158202131,536000,1292554158738131,10898139,1292554169636270,16163000,1292554185799270
+        -2143831735395280254,11.111000,"[NULL]","[NULL]",10878,1292554165562210,387000,10881,1292554165949210,363000,1292554166312210,148423,6,10882,1292554166460633,129000,1292554166589633,101000,10885,1292554166690633,1134000,10888,1292554167824633,1573498,7,10889,1292554169398131,545000,1292554169943131,10941139,1292554180884270,16107000,1292554196991270
+        -2143831735395280250,11.111000,"[NULL]","[NULL]",10799,1292554143168210,382000,10802,1292554143550210,355000,1292554143905210,122423,6,10803,1292554144027633,99000,1292554144126633,46000,10806,1292554144172633,1016000,10809,1292554145188633,1514498,7,10810,1292554146703131,483000,1292554147186131,11323139,1292554158509270,16182000,1292554174691270
+        -2143831735395280248,11.111000,"[NULL]","[NULL]",10991,1292554198448210,361000,10995,1292554198809210,317000,1292554199126210,167423,6,10996,1292554199293633,123000,1292554199416633,66000,11002,1292554199482633,1058000,11005,1292554200540633,1691498,7,11006,1292554202232131,543000,1292554202775131,11459139,1292554214234270,16001000,1292554230235270
+        -2143831735395280246,11.111000,"[NULL]","[NULL]",11033,1292554209783210,326000,11037,1292554210109210,338000,1292554210447210,139423,6,11038,1292554210586633,158000,1292554210744633,61000,11041,1292554210805633,1109000,11044,1292554211914633,763498,7,11045,1292554212678131,458000,1292554213136131,12006139,1292554225142270,16189000,1292554241331270
+        -2143831735395280244,11.111000,"[NULL]","[NULL]",10917,1292554176906210,371000,10920,1292554177277210,302000,1292554177579210,135423,6,10921,1292554177714633,105000,1292554177819633,47000,10924,1292554177866633,1033000,10927,1292554178899633,1364498,7,10928,1292554180264131,468000,1292554180732131,11126139,1292554191858270,16155000,1292554208013270
+        -2143831735395280242,11.111000,"[NULL]","[NULL]",10953,1292554187399210,383000,10959,1292554187782210,302000,1292554188084210,153423,6,10960,1292554188237633,149000,1292554188386633,57000,10963,1292554188443633,1080000,10966,1292554189523633,1628498,7,10967,1292554191152131,537000,1292554191689131,11488139,1292554203177270,15939000,1292554219116270
+        -2143831735395280239,11.111000,10616,1292554097735210,10629,1292554098654210,282000,10632,1292554098936210,237000,1292554099173210,121423,6,10633,1292554099294633,113000,1292554099407633,62000,10636,1292554099469633,953000,10641,1292554100422633,1211498,7,10643,1292554101634131,364000,1292554101998131,12589139,1292554114587270,15727000,1292554130314270
+        -2143831735395280229,11.111000,"[NULL]","[NULL]",10759,1292554132003210,421000,10762,1292554132424210,509000,1292554132933210,113423,6,10763,1292554133046633,120000,1292554133166633,96000,10766,1292554133262633,1095000,10769,1292554134357633,1469498,7,10770,1292554135827131,499000,1292554136326131,11256139,1292554147582270,16072000,1292554163654270
+        -2143831735395280227,11.111000,"[NULL]","[NULL]",10669,1292554109537210,306000,10677,1292554109843210,243000,1292554110086210,214423,6,10679,1292554110300633,119000,1292554110419633,42000,10682,1292554110461633,953000,10685,1292554111414633,731498,7,10686,1292554112146131,440000,1292554112586131,12791139,1292554125377270,15964000,1292554141341270
+        -2143831735395280226,11.111000,"[NULL]","[NULL]",10716,1292554120708210,290000,10721,1292554120998210,195000,1292554121193210,190423,6,10724,1292554121383633,89000,1292554121472633,42000,10727,1292554121514633,923000,10730,1292554122437633,1542498,7,10731,1292554123980131,426000,1292554124406131,12351139,1292554136757270,15793000,1292554152550270
+        -2143831735395280208,11.111000,"[NULL]","[NULL]",11150,1292554243173210,360000,11153,1292554243533210,219000,1292554243752210,267423,6,11154,1292554244019633,158000,1292554244177633,61000,11157,1292554244238633,1109000,11161,1292554245347633,1467498,7,11162,1292554246815131,558000,1292554247373131,11123139,1292554258496270,16184000,1292554274680270
+        -2143831735395280206,11.111000,"[NULL]","[NULL]",11189,1292554254651210,430000,11192,1292554255081210,201000,1292554255282210,299423,6,11193,1292554255581633,129000,1292554255710633,66000,11197,1292554255776633,1110000,11200,1292554256886633,1522498,7,11201,1292554258409131,591000,1292554259000131,10967139,1292554269967270,15807000,1292554285774270
+        -2143831735395280204,11.111000,"[NULL]","[NULL]",11066,1292554220890210,434000,11073,1292554221324210,355000,1292554221679210,197423,6,11074,1292554221876633,144000,1292554222020633,67000,11080,1292554222087633,1470000,11083,1292554223557633,1731498,7,11085,1292554225289131,658000,1292554225947131,10096139,1292554236043270,16406000,1292554252449270
+        -2143831735395280202,11.111000,"[NULL]","[NULL]",11109,1292554231758210,340000,11115,1292554232098210,296000,1292554232394210,127423,6,11116,1292554232521633,154000,1292554232675633,62000,11119,1292554232737633,1042000,11122,1292554233779633,1446498,7,11123,1292554235226131,479000,1292554235705131,11595139,1292554247300270,16249000,1292554263549270
+        -2143831735395280200,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+        -2143831735395280196,11.111000,"[NULL]","[NULL]",11229,1292554267072210,466000,11232,1292554267538210,202000,1292554267740210,469423,6,11233,1292554268209633,196000,1292554268405633,61000,11236,1292554268466633,1147000,11239,1292554269613633,2276498,7,11241,1292554271890131,695000,1292554272585131,8276139,1292554280861270,16049000,1292554296910270
+        -2143831735395280194,0.000000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+        -2143831735395280179,11.111000,10223,1292554042749210,10233,1292554043721210,451000,10239,1292554044172210,315000,1292554044487210,169423,6,10245,1292554044656633,670000,1292554045326633,1785000,10266,1292554047111633,1048000,10271,1292554048159633,1540498,7,10272,1292554049700131,458000,1292554050158131,8651139,1292554058809270,15896000,1292554074705270
+        -2143831735395280166,11.111000,10124,1292554030360210,10130,1292554030873210,568000,10135,1292554031441210,227000,1292554031668210,919423,6,10141,1292554032587633,754000,1292554033341633,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,15596000,1292554063604270
+        -2143831735395280153,11.111000,10469,1292554075561210,10481,1292554076592210,334000,10488,1292554076926210,301000,1292554077227210,177423,6,10494,1292554077404633,138000,1292554077542633,280000,10506,1292554077822633,988000,10509,1292554078810633,1377498,7,10516,1292554080188131,494000,1292554080682131,11265139,1292554091947270,16096000,1292554108043270
+        """))
 
   def test_chrome_scroll_update_info(self):
         return DiffTestBlueprint(
@@ -739,39 +737,82 @@
           viz_to_gpu_delay_dur,
           viz_swap_buffers_dur,
           latch_timestamp,
-          swap_end_timestamp,
           presentation_timestamp,
           viz_swap_buffers_to_latch_dur,
-          viz_latch_to_swap_end_dur,
-          swap_end_to_presentation_dur
+          viz_latch_to_presentation_dur
         FROM chrome_scroll_update_info
         ORDER BY id
         LIMIT 21
         """,
         out=Csv("""
-        "id","vsync_interval_ms","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","touch_move_received_ts","generation_to_browser_main_dur","scroll_update_created_ts","scroll_update_created_end_ts","touch_move_processing_dur","scroll_update_processing_dur","compositor_dispatch_ts","compositor_dispatch_end_ts","browser_to_compositor_delay_dur","compositor_dispatch_dur","compositor_on_begin_frame_ts","compositor_on_begin_frame_end_ts","compositor_dispatch_to_on_begin_frame_delay_dur","compositor_on_begin_frame_dur","compositor_generate_compositor_frame_ts","compositor_on_begin_frame_to_generation_delay_dur","compositor_submit_compositor_frame_ts","compositor_submit_compositor_frame_end_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_frame_dur","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_receive_compositor_frame_dur","viz_draw_and_swap_ts","viz_wait_for_draw_dur","viz_send_buffer_swap_end_ts","viz_draw_and_swap_dur","viz_swap_buffers_ts","viz_swap_buffers_end_ts","viz_to_gpu_delay_dur","viz_swap_buffers_dur","latch_timestamp","swap_end_timestamp","presentation_timestamp","viz_swap_buffers_to_latch_dur","viz_latch_to_swap_end_dur","swap_end_to_presentation_dur"
-        -2143831735395280256,11.111000,1,0,1,0,1,1292554141489270,"[NULL]","[NULL]",1292554142167257,1292554142530257,"[NULL]",363000,1292554143003210,1292554143111210,472953,108000,1292554154023210,1292554154106210,10912000,83000,1292554154282210,176000,1292554154619210,1292554154956210,337000,337000,1292554155095633,1292554155221633,139423,126000,1292554155286633,65000,1292554156581633,1295000,1292554158202131,1292554158738131,1620498,536000,1292554169636270,1292554176454270,1292554185799270,10898139,6818000,9345000
-        -2143831735395280254,11.111000,1,0,1,0,1,1292554152575270,"[NULL]","[NULL]",1292554154230257,1292554154489257,"[NULL]",259000,1292554155188210,1292554155308210,698953,120000,1292554164945210,1292554165168210,9637000,223000,1292554165562210,394000,1292554165949210,1292554166312210,387000,363000,1292554166460633,1292554166589633,148423,129000,1292554166690633,101000,1292554167824633,1134000,1292554169398131,1292554169943131,1573498,545000,1292554180884270,1292554187586270,1292554196991270,10941139,6702000,9405000
-        -2143831735395280250,11.111000,1,0,1,0,1,1292554130385270,"[NULL]","[NULL]",1292554131192257,1292554131471257,"[NULL]",279000,1292554131865210,1292554131963210,393953,98000,1292554142599210,1292554142790210,10636000,191000,1292554143168210,378000,1292554143550210,1292554143905210,382000,355000,1292554144027633,1292554144126633,122423,99000,1292554144172633,46000,1292554145188633,1016000,1292554146703131,1292554147186131,1514498,483000,1292554158509270,1292554165207270,1292554174691270,11323139,6698000,9484000
-        -2143831735395280248,11.111000,1,0,1,0,1,1292554185877270,"[NULL]","[NULL]",1292554186628257,1292554187026257,"[NULL]",398000,1292554187244210,1292554187351210,217953,107000,1292554198200210,1292554198282210,10849000,82000,1292554198448210,166000,1292554198809210,1292554199126210,361000,317000,1292554199293633,1292554199416633,167423,123000,1292554199482633,66000,1292554200540633,1058000,1292554202232131,1292554202775131,1691498,543000,1292554214234270,1292554221192270,1292554230235270,11459139,6958000,9043000
-        -2143831735395280246,11.111000,1,0,1,0,1,1292554196968270,"[NULL]","[NULL]",1292554198042257,1292554198404257,"[NULL]",362000,1292554199295210,1292554199405210,890953,110000,1292554209368210,1292554209458210,9963000,90000,1292554209783210,325000,1292554210109210,1292554210447210,326000,338000,1292554210586633,1292554210744633,139423,158000,1292554210805633,61000,1292554211914633,1109000,1292554212678131,1292554213136131,763498,458000,1292554225142270,1292554231869270,1292554241331270,12006139,6727000,9462000
-        -2143831735395280244,11.111000,1,0,1,0,1,1292554163682270,"[NULL]","[NULL]",1292554164468257,1292554164861257,"[NULL]",393000,1292554165375210,1292554165502210,513953,127000,1292554176300210,1292554176526210,10798000,226000,1292554176906210,380000,1292554177277210,1292554177579210,371000,302000,1292554177714633,1292554177819633,135423,105000,1292554177866633,47000,1292554178899633,1033000,1292554180264131,1292554180732131,1364498,468000,1292554191858270,1292554198113270,1292554208013270,11126139,6255000,9900000
-        -2143831735395280242,11.111000,1,0,1,0,1,1292554174786270,"[NULL]","[NULL]",1292554175708257,1292554176029257,"[NULL]",321000,1292554176727210,1292554176834210,697953,107000,1292554187011210,1292554187099210,10177000,88000,1292554187399210,300000,1292554187782210,1292554188084210,383000,302000,1292554188237633,1292554188386633,153423,149000,1292554188443633,57000,1292554189523633,1080000,1292554191152131,1292554191689131,1628498,537000,1292554203177270,1292554209528270,1292554219116270,11488139,6351000,9588000
-        -2143831735395280239,11.111000,1,0,1,0,1,1292554086893270,"[NULL]","[NULL]",1292554086897257,1292554087025257,"[NULL]",128000,1292554088316210,1292554088395210,1290953,79000,1292554097735210,1292554098425210,9340000,690000,1292554098654210,229000,1292554098936210,1292554099173210,282000,237000,1292554099294633,1292554099407633,121423,113000,1292554099469633,62000,1292554100422633,953000,1292554101634131,1292554101998131,1211498,364000,1292554114587270,1292554120289270,1292554130314270,12589139,5702000,10025000
-        -2143831735395280229,11.111000,1,0,1,0,1,1292554119302270,"[NULL]","[NULL]",1292554120042257,1292554120369257,"[NULL]",327000,1292554120537210,1292554120631210,167953,94000,1292554131566210,1292554131724210,10935000,158000,1292554132003210,279000,1292554132424210,1292554132933210,421000,509000,1292554133046633,1292554133166633,113423,120000,1292554133262633,96000,1292554134357633,1095000,1292554135827131,1292554136326131,1469498,499000,1292554147582270,1292554154188270,1292554163654270,11256139,6606000,9466000
-        -2143831735395280227,11.111000,1,0,1,0,1,1292554097138270,"[NULL]","[NULL]",1292554097987257,1292554098176257,"[NULL]",189000,1292554098543210,1292554098619210,366953,76000,1292554109098210,1292554109249210,10479000,151000,1292554109537210,288000,1292554109843210,1292554110086210,306000,243000,1292554110300633,1292554110419633,214423,119000,1292554110461633,42000,1292554111414633,953000,1292554112146131,1292554112586131,731498,440000,1292554125377270,1292554131616270,1292554141341270,12791139,6239000,9725000
-        -2143831735395280226,11.111000,1,0,1,0,1,1292554108216270,"[NULL]","[NULL]",1292554108988257,1292554109310257,"[NULL]",322000,1292554109391210,1292554109491210,80953,100000,1292554120251210,1292554120389210,10760000,138000,1292554120708210,319000,1292554120998210,1292554121193210,290000,195000,1292554121383633,1292554121472633,190423,89000,1292554121514633,42000,1292554122437633,923000,1292554123980131,1292554124406131,1542498,426000,1292554136757270,1292554143046270,1292554152550270,12351139,6289000,9504000
-        -2143831735395280208,11.111000,1,0,1,0,1,1292554230251270,"[NULL]","[NULL]",1292554231054257,1292554231462257,"[NULL]",408000,1292554231608210,1292554231711210,145953,103000,1292554242726210,1292554242854210,11015000,128000,1292554243173210,319000,1292554243533210,1292554243752210,360000,219000,1292554244019633,1292554244177633,267423,158000,1292554244238633,61000,1292554245347633,1109000,1292554246815131,1292554247373131,1467498,558000,1292554258496270,1292554265091270,1292554274680270,11123139,6595000,9589000
-        -2143831735395280206,11.111000,1,0,1,0,1,1292554241443270,"[NULL]","[NULL]",1292554242336257,1292554242660257,"[NULL]",324000,1292554242996210,1292554243116210,335953,120000,1292554254186210,1292554254324210,11070000,138000,1292554254651210,327000,1292554255081210,1292554255282210,430000,201000,1292554255581633,1292554255710633,299423,129000,1292554255776633,66000,1292554256886633,1110000,1292554258409131,1292554259000131,1522498,591000,1292554269967270,1292554276658270,1292554285774270,10967139,6691000,9116000
-        -2143831735395280204,11.111000,1,0,1,0,1,1292554208072270,"[NULL]","[NULL]",1292554208931257,1292554209188257,"[NULL]",257000,1292554209612210,1292554209722210,423953,110000,1292554220579210,1292554220686210,10857000,107000,1292554220890210,204000,1292554221324210,1292554221679210,434000,355000,1292554221876633,1292554222020633,197423,144000,1292554222087633,67000,1292554223557633,1470000,1292554225289131,1292554225947131,1731498,658000,1292554236043270,1292554243002270,1292554252449270,10096139,6959000,9447000
-        -2143831735395280202,11.111000,1,0,1,0,1,1292554219159270,"[NULL]","[NULL]",1292554220303257,1292554220678257,"[NULL]",375000,1292554221904210,1292554222054210,1225953,150000,1292554231391210,1292554231468210,9337000,77000,1292554231758210,290000,1292554232098210,1292554232394210,340000,296000,1292554232521633,1292554232675633,127423,154000,1292554232737633,62000,1292554233779633,1042000,1292554235226131,1292554235705131,1446498,479000,1292554247300270,1292554253765270,1292554263549270,11595139,6465000,9784000
-        -2143831735395280200,0.000000,0,0,1,0,1,1292554274773270,"[NULL]","[NULL]",1292554275745257,1292554276049257,"[NULL]",304000,1292554276887210,1292554277031210,837953,144000,1292554286887210,1292554287129210,9856000,242000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
-        -2143831735395280196,11.111000,1,0,1,0,1,1292554252553270,"[NULL]","[NULL]",1292554253301257,1292554253646257,"[NULL]",345000,1292554254466210,1292554254585210,819953,119000,1292554266517210,1292554266634210,11932000,117000,1292554267072210,438000,1292554267538210,1292554267740210,466000,202000,1292554268209633,1292554268405633,469423,196000,1292554268466633,61000,1292554269613633,1147000,1292554271890131,1292554272585131,2276498,695000,1292554280861270,1292554287004270,1292554296910270,8276139,6143000,9906000
-        -2143831735395280194,0.000000,0,0,1,0,1,1292554263653270,"[NULL]","[NULL]",1292554264600257,1292554264879257,"[NULL]",279000,1292554266795210,1292554266988210,1915953,193000,1292554276544210,1292554276677210,9556000,133000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
-        -2143831735395280183,11.111000,0,0,0,0,0,1292554034979270,1292554038935257,3955987,1292554039221257,1292554039362257,286000,141000,1292554039380210,1292554039504210,17953,124000,1292554042749210,1292554043545210,3245000,796000,1292554043721210,176000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554065670270,1292554074705270,8651139,6861000,9035000
-        -2143831735395280179,11.111000,1,0,0,0,1,1292554029441270,1292554037281257,7839987,1292554037618257,1292554037785257,337000,167000,1292554038237210,1292554038326210,451953,89000,1292554042749210,1292554043429210,4423000,680000,1292554043721210,292000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554065670270,1292554074705270,8651139,6861000,9035000
-        -2143831735395280166,11.111000,1,0,0,1,1,1292554023976270,1292554027681257,3704987,1292554029847257,1292554030083257,2166000,236000,1292554030360210,1292554030737210,276953,377000,1292554030360210,1292554030725210,-377000,365000,1292554030873210,148000,1292554031441210,1292554031668210,568000,227000,1292554032587633,1292554033341633,919423,754000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,1292554053830270,1292554063604270,"[NULL]",5822000,9774000
-  """))
+        "id","vsync_interval_ms","is_presented","is_janky","is_inertial","is_first_scroll_update_in_scroll","is_first_scroll_update_in_frame","generation_ts","touch_move_received_ts","generation_to_browser_main_dur","scroll_update_created_ts","scroll_update_created_end_ts","touch_move_processing_dur","scroll_update_processing_dur","compositor_dispatch_ts","compositor_dispatch_end_ts","browser_to_compositor_delay_dur","compositor_dispatch_dur","compositor_on_begin_frame_ts","compositor_on_begin_frame_end_ts","compositor_dispatch_to_on_begin_frame_delay_dur","compositor_on_begin_frame_dur","compositor_generate_compositor_frame_ts","compositor_on_begin_frame_to_generation_delay_dur","compositor_submit_compositor_frame_ts","compositor_submit_compositor_frame_end_ts","compositor_generate_frame_to_submit_frame_dur","compositor_submit_frame_dur","viz_receive_compositor_frame_ts","viz_receive_compositor_frame_end_ts","compositor_to_viz_delay_dur","viz_receive_compositor_frame_dur","viz_draw_and_swap_ts","viz_wait_for_draw_dur","viz_send_buffer_swap_end_ts","viz_draw_and_swap_dur","viz_swap_buffers_ts","viz_swap_buffers_end_ts","viz_to_gpu_delay_dur","viz_swap_buffers_dur","latch_timestamp","presentation_timestamp","viz_swap_buffers_to_latch_dur","viz_latch_to_presentation_dur"
+        -2143831735395280256,11.111000,1,0,1,0,1,1292554141489270,"[NULL]",677987,1292554142167257,1292554142530257,"[NULL]",363000,1292554143003210,1292554143111210,472953,108000,1292554154023210,1292554154106210,10912000,83000,1292554154282210,176000,1292554154619210,1292554154956210,337000,337000,1292554155095633,1292554155221633,139423,126000,1292554155286633,65000,1292554156581633,1295000,1292554158202131,1292554158738131,1620498,536000,1292554169636270,1292554185799270,10898139,16163000
+        -2143831735395280254,11.111000,1,0,1,0,1,1292554152575270,"[NULL]",1654987,1292554154230257,1292554154489257,"[NULL]",259000,1292554155188210,1292554155308210,698953,120000,1292554164945210,1292554165168210,9637000,223000,1292554165562210,394000,1292554165949210,1292554166312210,387000,363000,1292554166460633,1292554166589633,148423,129000,1292554166690633,101000,1292554167824633,1134000,1292554169398131,1292554169943131,1573498,545000,1292554180884270,1292554196991270,10941139,16107000
+        -2143831735395280250,11.111000,1,0,1,0,1,1292554130385270,"[NULL]",806987,1292554131192257,1292554131471257,"[NULL]",279000,1292554131865210,1292554131963210,393953,98000,1292554142599210,1292554142790210,10636000,191000,1292554143168210,378000,1292554143550210,1292554143905210,382000,355000,1292554144027633,1292554144126633,122423,99000,1292554144172633,46000,1292554145188633,1016000,1292554146703131,1292554147186131,1514498,483000,1292554158509270,1292554174691270,11323139,16182000
+        -2143831735395280248,11.111000,1,0,1,0,1,1292554185877270,"[NULL]",750987,1292554186628257,1292554187026257,"[NULL]",398000,1292554187244210,1292554187351210,217953,107000,1292554198200210,1292554198282210,10849000,82000,1292554198448210,166000,1292554198809210,1292554199126210,361000,317000,1292554199293633,1292554199416633,167423,123000,1292554199482633,66000,1292554200540633,1058000,1292554202232131,1292554202775131,1691498,543000,1292554214234270,1292554230235270,11459139,16001000
+        -2143831735395280246,11.111000,1,0,1,0,1,1292554196968270,"[NULL]",1073987,1292554198042257,1292554198404257,"[NULL]",362000,1292554199295210,1292554199405210,890953,110000,1292554209368210,1292554209458210,9963000,90000,1292554209783210,325000,1292554210109210,1292554210447210,326000,338000,1292554210586633,1292554210744633,139423,158000,1292554210805633,61000,1292554211914633,1109000,1292554212678131,1292554213136131,763498,458000,1292554225142270,1292554241331270,12006139,16189000
+        -2143831735395280244,11.111000,1,0,1,0,1,1292554163682270,"[NULL]",785987,1292554164468257,1292554164861257,"[NULL]",393000,1292554165375210,1292554165502210,513953,127000,1292554176300210,1292554176526210,10798000,226000,1292554176906210,380000,1292554177277210,1292554177579210,371000,302000,1292554177714633,1292554177819633,135423,105000,1292554177866633,47000,1292554178899633,1033000,1292554180264131,1292554180732131,1364498,468000,1292554191858270,1292554208013270,11126139,16155000
+        -2143831735395280242,11.111000,1,0,1,0,1,1292554174786270,"[NULL]",921987,1292554175708257,1292554176029257,"[NULL]",321000,1292554176727210,1292554176834210,697953,107000,1292554187011210,1292554187099210,10177000,88000,1292554187399210,300000,1292554187782210,1292554188084210,383000,302000,1292554188237633,1292554188386633,153423,149000,1292554188443633,57000,1292554189523633,1080000,1292554191152131,1292554191689131,1628498,537000,1292554203177270,1292554219116270,11488139,15939000
+        -2143831735395280239,11.111000,1,0,1,0,1,1292554086893270,"[NULL]",3987,1292554086897257,1292554087025257,"[NULL]",128000,1292554088316210,1292554088395210,1290953,79000,1292554097735210,1292554098425210,9340000,690000,1292554098654210,229000,1292554098936210,1292554099173210,282000,237000,1292554099294633,1292554099407633,121423,113000,1292554099469633,62000,1292554100422633,953000,1292554101634131,1292554101998131,1211498,364000,1292554114587270,1292554130314270,12589139,15727000
+        -2143831735395280229,11.111000,1,0,1,0,1,1292554119302270,"[NULL]",739987,1292554120042257,1292554120369257,"[NULL]",327000,1292554120537210,1292554120631210,167953,94000,1292554131566210,1292554131724210,10935000,158000,1292554132003210,279000,1292554132424210,1292554132933210,421000,509000,1292554133046633,1292554133166633,113423,120000,1292554133262633,96000,1292554134357633,1095000,1292554135827131,1292554136326131,1469498,499000,1292554147582270,1292554163654270,11256139,16072000
+        -2143831735395280227,11.111000,1,0,1,0,1,1292554097138270,"[NULL]",848987,1292554097987257,1292554098176257,"[NULL]",189000,1292554098543210,1292554098619210,366953,76000,1292554109098210,1292554109249210,10479000,151000,1292554109537210,288000,1292554109843210,1292554110086210,306000,243000,1292554110300633,1292554110419633,214423,119000,1292554110461633,42000,1292554111414633,953000,1292554112146131,1292554112586131,731498,440000,1292554125377270,1292554141341270,12791139,15964000
+        -2143831735395280226,11.111000,1,0,1,0,1,1292554108216270,"[NULL]",771987,1292554108988257,1292554109310257,"[NULL]",322000,1292554109391210,1292554109491210,80953,100000,1292554120251210,1292554120389210,10760000,138000,1292554120708210,319000,1292554120998210,1292554121193210,290000,195000,1292554121383633,1292554121472633,190423,89000,1292554121514633,42000,1292554122437633,923000,1292554123980131,1292554124406131,1542498,426000,1292554136757270,1292554152550270,12351139,15793000
+        -2143831735395280208,11.111000,1,0,1,0,1,1292554230251270,"[NULL]",802987,1292554231054257,1292554231462257,"[NULL]",408000,1292554231608210,1292554231711210,145953,103000,1292554242726210,1292554242854210,11015000,128000,1292554243173210,319000,1292554243533210,1292554243752210,360000,219000,1292554244019633,1292554244177633,267423,158000,1292554244238633,61000,1292554245347633,1109000,1292554246815131,1292554247373131,1467498,558000,1292554258496270,1292554274680270,11123139,16184000
+        -2143831735395280206,11.111000,1,0,1,0,1,1292554241443270,"[NULL]",892987,1292554242336257,1292554242660257,"[NULL]",324000,1292554242996210,1292554243116210,335953,120000,1292554254186210,1292554254324210,11070000,138000,1292554254651210,327000,1292554255081210,1292554255282210,430000,201000,1292554255581633,1292554255710633,299423,129000,1292554255776633,66000,1292554256886633,1110000,1292554258409131,1292554259000131,1522498,591000,1292554269967270,1292554285774270,10967139,15807000
+        -2143831735395280204,11.111000,1,0,1,0,1,1292554208072270,"[NULL]",858987,1292554208931257,1292554209188257,"[NULL]",257000,1292554209612210,1292554209722210,423953,110000,1292554220579210,1292554220686210,10857000,107000,1292554220890210,204000,1292554221324210,1292554221679210,434000,355000,1292554221876633,1292554222020633,197423,144000,1292554222087633,67000,1292554223557633,1470000,1292554225289131,1292554225947131,1731498,658000,1292554236043270,1292554252449270,10096139,16406000
+        -2143831735395280202,11.111000,1,0,1,0,1,1292554219159270,"[NULL]",1143987,1292554220303257,1292554220678257,"[NULL]",375000,1292554221904210,1292554222054210,1225953,150000,1292554231391210,1292554231468210,9337000,77000,1292554231758210,290000,1292554232098210,1292554232394210,340000,296000,1292554232521633,1292554232675633,127423,154000,1292554232737633,62000,1292554233779633,1042000,1292554235226131,1292554235705131,1446498,479000,1292554247300270,1292554263549270,11595139,16249000
+        -2143831735395280200,0.000000,0,0,1,0,1,1292554274773270,"[NULL]",971987,1292554275745257,1292554276049257,"[NULL]",304000,1292554276887210,1292554277031210,837953,144000,1292554286887210,1292554287129210,9856000,242000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+        -2143831735395280196,11.111000,1,0,1,0,1,1292554252553270,"[NULL]",747987,1292554253301257,1292554253646257,"[NULL]",345000,1292554254466210,1292554254585210,819953,119000,1292554266517210,1292554266634210,11932000,117000,1292554267072210,438000,1292554267538210,1292554267740210,466000,202000,1292554268209633,1292554268405633,469423,196000,1292554268466633,61000,1292554269613633,1147000,1292554271890131,1292554272585131,2276498,695000,1292554280861270,1292554296910270,8276139,16049000
+        -2143831735395280194,0.000000,0,0,1,0,1,1292554263653270,"[NULL]",946987,1292554264600257,1292554264879257,"[NULL]",279000,1292554266795210,1292554266988210,1915953,193000,1292554276544210,1292554276677210,9556000,133000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]"
+        -2143831735395280183,11.111000,0,0,0,0,0,1292554034979270,1292554038935257,3955987,1292554039221257,1292554039362257,286000,141000,1292554039380210,1292554039504210,17953,124000,1292554042749210,1292554043545210,3245000,796000,1292554043721210,176000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554074705270,8651139,15896000
+        -2143831735395280179,11.111000,1,0,0,0,1,1292554029441270,1292554037281257,7839987,1292554037618257,1292554037785257,337000,167000,1292554038237210,1292554038326210,451953,89000,1292554042749210,1292554043429210,4423000,680000,1292554043721210,292000,1292554044172210,1292554044487210,451000,315000,1292554044656633,1292554045326633,169423,670000,1292554047111633,1785000,1292554048159633,1048000,1292554049700131,1292554050158131,1540498,458000,1292554058809270,1292554074705270,8651139,15896000
+        -2143831735395280166,11.111000,1,0,0,1,1,1292554023976270,1292554027681257,3704987,1292554029847257,1292554030083257,2166000,236000,1292554030360210,1292554030737210,276953,377000,1292554030360210,1292554030725210,-377000,365000,1292554030873210,148000,1292554031441210,1292554031668210,568000,227000,1292554032587633,1292554033341633,919423,754000,"[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]","[NULL]",1292554048008270,1292554063604270,"[NULL]",15596000
+        """))
+
+  def test_chrome_scroll_update_info_step_templates(self):
+        # Verify that chrome_scroll_update_info_step_templates references at
+        # least one valid column name and no invalid column names in
+        # chrome_scroll_update_info.
+        return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+
+        WITH referenced_column_names AS (
+          SELECT
+            ts_column_name AS column_name
+          FROM chrome_scroll_update_info_step_templates
+          WHERE column_name IS NOT NULL
+          UNION ALL
+          SELECT
+            dur_column_name AS column_name
+          FROM chrome_scroll_update_info_step_templates
+          WHERE column_name IS NOT NULL
+        ),
+        valid_column_names AS (
+          SELECT name AS column_name
+          FROM pragma_table_info('chrome_scroll_update_info')
+        )
+        SELECT
+          "valid" AS validity,
+          EXISTS (
+            SELECT column_name FROM referenced_column_names
+            WHERE column_name IN valid_column_names
+          ) AS existence
+        UNION ALL
+        SELECT
+          "invalid" AS validity,
+          EXISTS (
+            SELECT column_name FROM referenced_column_names
+            WHERE column_name NOT IN valid_column_names
+          ) AS existence
+        ORDER BY validity DESC;
+        """,
+        out=Csv("""
+        "validity","existence"
+        "valid",1
+        "invalid",0
+        """))
 
   # A trace from M132 (ToT as of adding this test) has the necessary
   # events/arguments (including the ones from the 'view' atrace category).
diff --git a/test/trace_processor/diff_tests/stdlib/linux/tests.py b/test/trace_processor/diff_tests/stdlib/linux/tests.py
index 783625f..4093c2a 100644
--- a/test/trace_processor/diff_tests/stdlib/linux/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/linux/tests.py
@@ -28,23 +28,23 @@
         query="""
         INCLUDE PERFETTO MODULE linux.threads;
 
-        SELECT upid, utid, pid, tid, process_name, thread_name
+        SELECT pid, tid, process_name, thread_name
         FROM linux_kernel_threads
-        ORDER by utid LIMIT 10;
+        ORDER by tid LIMIT 10;
         """,
         out=Csv("""
-        "upid","utid","pid","tid","process_name","thread_name"
-        7,14,510,510,"sugov:0","sugov:0"
-        89,23,1365,1365,"com.google.usf.","com.google.usf."
-        87,37,1249,1249,"irq/357-dwc3","irq/357-dwc3"
-        31,38,6,6,"kworker/u16:0","kworker/u16:0"
-        11,42,511,511,"sugov:4","sugov:4"
-        83,43,1152,1152,"irq/502-fts_ts","irq/502-fts_ts"
-        93,44,2374,2374,"csf_sync_update","csf_sync_update"
-        18,45,2379,2379,"csf_kcpu_0","csf_kcpu_0"
-        12,47,247,247,"decon0_kthread","decon0_kthread"
-        65,48,159,159,"spi0","spi0"
-            """))
+            "pid","tid","process_name","thread_name"
+            2,2,"kthreadd","kthreadd"
+            5,5,"kworker/0:0H","kworker/0:0H"
+            6,6,"kworker/u16:0","kworker/u16:0"
+            8,8,"kworker/u16:1","kworker/u16:1"
+            11,11,"ksoftirqd/0","ksoftirqd/0"
+            12,12,"rcu_preempt","rcu_preempt"
+            13,13,"rcuog/0","rcuog/0"
+            14,14,"rcuop/0","rcuop/0"
+            15,15,"rcub/0","rcub/0"
+            17,17,"rcu_exp_gp_kthr","rcu_exp_gp_kthr"
+        """))
 
   # Tests that DSU devfreq counters are working properly
   def test_dsu_devfreq(self):
diff --git a/test/trace_processor/diff_tests/stdlib/viz/tests.py b/test/trace_processor/diff_tests/stdlib/viz/tests.py
index 0b986bb..ae1bf05 100644
--- a/test/trace_processor/diff_tests/stdlib/viz/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/viz/tests.py
@@ -271,13 +271,14 @@
     return DiffTestBlueprint(
         trace=self.chronological_trace,
         query="""
-        INCLUDE PERFETTO MODULE viz.summary.tracks;
-        SELECT id, order_id
-        FROM _track_event_tracks_ordered
-        ORDER BY id;
+        INCLUDE PERFETTO MODULE viz.summary.track_event;
+        SELECT cast_int!(track_ids) as id, order_id
+        FROM _track_event_tracks_ordered_groups
+        ORDER BY track_ids;
         """,
         out=Csv("""
         "id","order_id"
+        0,1
         1,2
         2,1
         """))
@@ -305,13 +306,14 @@
     return DiffTestBlueprint(
         trace=self.explicit_trace,
         query="""
-        INCLUDE PERFETTO MODULE viz.summary.tracks;
-        SELECT id, order_id
-        FROM _track_event_tracks_ordered
+        INCLUDE PERFETTO MODULE viz.summary.track_event;
+        SELECT cast_int!(track_ids) as id, order_id
+        FROM _track_event_tracks_ordered_groups
         ORDER BY id;
         """,
         out=Csv("""
         "id","order_id"
+        0,1
         1,2
         2,3
         3,1
@@ -341,13 +343,14 @@
     return DiffTestBlueprint(
         trace=self.lexicographic_trace,
         query="""
-        INCLUDE PERFETTO MODULE viz.summary.tracks;
-        SELECT id, order_id
-        FROM _track_event_tracks_ordered
+        INCLUDE PERFETTO MODULE viz.summary.track_event;
+        SELECT cast_int!(track_ids) as id, order_id
+        FROM _track_event_tracks_ordered_groups
         ORDER BY id;
         """,
         out=Csv("""
         "id","order_id"
+        0,1
         1,2
         2,1
         3,3
@@ -385,14 +388,16 @@
     return DiffTestBlueprint(
         trace=self.all_ordering_trace,
         query="""
-        INCLUDE PERFETTO MODULE viz.summary.tracks;
-        SELECT id, parent_id, order_id
-        FROM _track_event_tracks_ordered
-        JOIN track USING (id)
+        INCLUDE PERFETTO MODULE viz.summary.track_event;
+        SELECT cast_int!(track_ids) as id, parent_id, order_id
+        FROM _track_event_tracks_ordered_groups
         ORDER BY parent_id, id
         """,
         out=Csv("""
         "id","parent_id","order_id"
+        0,"[NULL]",1
+        3,"[NULL]",3
+        5,"[NULL]",2
         1,0,2
         2,0,1
         4,3,1
@@ -438,20 +443,22 @@
     return DiffTestBlueprint(
         trace=Path('track_event_tracks_ordering.textproto'),
         query="""
-        INCLUDE PERFETTO MODULE viz.summary.tracks;
-        SELECT id, order_id
-        FROM _track_event_tracks_ordered
+        INCLUDE PERFETTO MODULE viz.summary.track_event;
+        SELECT cast_int!(track_ids) as id, order_id
+        FROM _track_event_tracks_ordered_groups
         ORDER BY id;
         """,
         out=Csv("""
         "id","order_id"
+        0,2
         1,3
         2,4
         3,1
+        4,1
         5,1
         6,2
         7,3
-        8,2
+        9,3
         10,1
         11,2
         12,4
diff --git a/test/trace_processor/diff_tests/stdlib/wattson/tests.py b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
index 1f86878..b13a767 100644
--- a/test/trace_processor/diff_tests/stdlib/wattson/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
@@ -161,7 +161,7 @@
         out=Csv("""
             "duration","freq_0","idle_0","freq_1","idle_1","freq_2","idle_2","freq_3","idle_3","suspended"
             16606175990,614400,1,614400,1,614400,1,614400,1,0
-            10648392546,1708800,-1,1708800,-1,1708800,-1,1708800,-1,1
+            10648392546,1708800,1,1708800,1,1708800,1,1708800,1,1
             6972220533,1708800,-1,1708800,-1,1708800,-1,1708800,-1,0
             1649400745,614400,0,614400,0,614400,0,614400,0,0
             1206977074,614400,-1,614400,1,614400,1,614400,1,0
@@ -451,10 +451,10 @@
             """),
         out=Csv("""
             "ts","dur","cpu0_id","cpu1_id","cpu2_id","cpu3_id","suspended"
-            385019771468,61975407053,12041,12218,10488,8910,1
-            448320364476,3674872885,13005,12954,11166,9272,1
-            452415394221,69579176303,13654,13361,11651,9609,1
-            564873995228,135118729231,45223,37594,22798,20132,1
+            385019771468,61975407053,12042,12219,10489,8911,1
+            448320364476,3674872885,13008,12957,11169,9275,1
+            452415394221,69579176303,13659,13366,11656,9614,1
+            564873995228,135118729231,45230,37601,22805,20139,1
             """))
 
   # Tests traces from VM that have incomplete CPU tracks
@@ -485,7 +485,7 @@
             """))
 
   # Tests suspend path with devfreq code path
-  def test_wattson_devfreq_suspend(self):
+  def test_wattson_devfreq_hotplug_and_suspend(self):
     return DiffTestBlueprint(
         trace=DataPath('wattson_cpuhp_devfreq_suspend.pb'),
         query=("""
@@ -494,14 +494,16 @@
                  ts, dur, cpu0_mw, cpu1_mw, cpu2_mw, cpu3_mw, cpu4_mw, cpu5_mw,
                  cpu6_mw, cpu7_mw, dsu_scu_mw
                FROM _system_state_mw
-               WHERE ts > 165725472126
-              LIMIT 4
+               WHERE ts > 165725449108
+              LIMIT 6
             """),
         out=Csv("""
             "ts","dur","cpu0_mw","cpu1_mw","cpu2_mw","cpu3_mw","cpu4_mw","cpu5_mw","cpu6_mw","cpu7_mw","dsu_scu_mw"
-            165725475055,6999,0.000000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
-            165725482054,1546,111.020000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
-            165725483600,4468465,111.020000,111.020000,111.020000,111.020000,267.180000,267.180000,267.180000,375.490000,14.560000
+            165725450194,7527,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,375.490000,14.560000
+            165725457721,17334,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
+            165725475055,6999,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
+            165725482054,1546,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
+            165725483600,4468465,111.020000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,14.560000
             165729952065,73480460119,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
             """))
 
@@ -529,3 +531,28 @@
                1450347293559,715573,3,0
                1450348009132,82292,3,-1
                """))
+
+  # Tests that hotplug slices that defined CPU off region are correct
+  def test_wattson_hotplug_tk(self):
+    return DiffTestBlueprint(
+        trace=DataPath('wattson_cpuhp_devfreq_suspend.pb'),
+        query=("""
+            INCLUDE PERFETTO MODULE wattson.cpu_hotplug;
+            SELECT cpu, ts, dur
+            FROM _gapless_hotplug_slices
+            WHERE cpu < 2
+            """),
+        out=Csv("""
+            "cpu","ts","dur"
+            0,86747008512,302795933205
+            1,86747008512,3769632400
+            1,90516640912,4341919
+            1,90520982831,73692291133
+            1,164213273964,1478796428
+            1,165692070392,73525895666
+            1,239217966058,10896074956
+            1,250114041014,95948
+            1,250114136962,4705159
+            1,250118842121,137102890041
+            1,387221732162,2321209555
+            """))
diff --git a/test/trace_processor/diff_tests/tables/counter_dur_test.sql b/test/trace_processor/diff_tests/tables/counter_dur_test.sql
deleted file mode 100644
index 67a60b9..0000000
--- a/test/trace_processor/diff_tests/tables/counter_dur_test.sql
+++ /dev/null
@@ -1 +0,0 @@
-SELECT ts, dur FROM experimental_counter_dur WHERE track_id IN (1, 2, 3) ORDER BY dur LIMIT 10;
diff --git a/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index c5454dd..7d6b150 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -402,8 +402,8 @@
         query="SELECT id, slice_out, slice_in, trace_id, arg_set_id FROM flow;",
         out=Csv("""
           "id","slice_out","slice_in","trace_id","arg_set_id"
-          0,0,1,57,0
-          1,1,2,57,0
+          0,0,1,57,"[NULL]"
+          1,1,2,57,"[NULL]"
         """))
 
   def test_clock_snapshot_table_multiplier(self):
diff --git a/test/trace_processor/diff_tests/tables/tests_counters.py b/test/trace_processor/diff_tests/tables/tests_counters.py
index fe3f9fb..ace4cf9 100644
--- a/test/trace_processor/diff_tests/tables/tests_counters.py
+++ b/test/trace_processor/diff_tests/tables/tests_counters.py
@@ -108,52 +108,6 @@
         """,
         out=Path('filter_row_vector_example_android_trace_30s.out'))
 
-  def test_counter_dur_example_android_trace_30s(self):
-    return DiffTestBlueprint(
-        trace=DataPath('example_android_trace_30s.pb'),
-        query=Path('counter_dur_test.sql'),
-        out=Csv("""
-        "ts","dur"
-        100351738640,-1
-        100351738640,-1
-        100351738640,-1
-        70731059648,19510835
-        70731059648,19510835
-        70731059648,19510835
-        73727335051,23522762
-        73727335051,23522762
-        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, m.raw_id as raw_machine_id
-        FROM experimental_counter_dur c
-        JOIN counter_track t on c.track_id = t.id
-        JOIN machine m on t.machine_id = m.id
-        WHERE track_id IN (1, 2, 3)
-        ORDER BY dur LIMIT 10;
-        """,
-        out=Csv("""
-        "ts","dur","raw_machine_id"
-        100351738640,-1,1001
-        100351738640,-1,1001
-        100351738640,-1,1001
-        70731059648,19510835,1001
-        70731059648,19510835,1001
-        70731059648,19510835,1001
-        73727335051,23522762,1001
-        73727335051,23522762,1001
-        73727335051,23522762,1001
-        86726132752,24487554,1001
-        """))
-
   # Tests counter.machine_id and process_counter_track.machine.
   def test_filter_row_vector_example_android_trace_30s_machine_id(self):
     return DiffTestBlueprint(
@@ -287,8 +241,8 @@
         """,
         out=Csv("""
         "id","arg_set_id"
-        1,1
-        15,8
+        1,0
+        15,14
         """))
 
   def test_cpu_counter_track_multi_machine(self):
diff --git a/test/trace_processor/diff_tests/tables/tests_sched.py b/test/trace_processor/diff_tests/tables/tests_sched.py
index 82ec0bd..c1416d1 100644
--- a/test/trace_processor/diff_tests/tables/tests_sched.py
+++ b/test/trace_processor/diff_tests/tables/tests_sched.py
@@ -120,27 +120,27 @@
         15748
         """))
 
-  def test_raw_common_flags(self):
+  def test_ftrace_event_common_flags(self):
     return DiffTestBlueprint(
         trace=DataPath('sched_wakeup_trace.atr'),
         query="""
           SELECT id, ts, name, cpu, utid, arg_set_id, common_flags
-          FROM raw
+          FROM ftrace_event
           WHERE common_flags != 0
           ORDER BY ts LIMIT 10
         """,
         out=Csv("""
-        "id","ts","name","cpu","utid","arg_set_id","common_flags"
-        3,1735489788930,"sched_waking",0,300,4,1
-        4,1735489812571,"sched_waking",0,300,5,1
-        5,1735489833977,"sched_waking",1,305,6,1
-        8,1735489876788,"sched_waking",1,297,9,1
-        9,1735489879097,"sched_waking",0,304,10,1
-        12,1735489933912,"sched_waking",0,428,13,1
-        14,1735489972385,"sched_waking",1,232,15,1
-        17,1735489999987,"sched_waking",1,232,15,1
-        19,1735490039439,"sched_waking",1,298,18,1
-        20,1735490042084,"sched_waking",1,298,19,1
+          "id","ts","name","cpu","utid","arg_set_id","common_flags"
+          3,1735489788930,"sched_waking",0,300,21,1
+          4,1735489812571,"sched_waking",0,300,25,1
+          5,1735489833977,"sched_waking",1,305,29,1
+          8,1735489876788,"sched_waking",1,297,47,1
+          9,1735489879097,"sched_waking",0,304,51,1
+          12,1735489933912,"sched_waking",0,428,69,1
+          14,1735489972385,"sched_waking",1,232,80,1
+          17,1735489999987,"sched_waking",1,232,80,1
+          19,1735490039439,"sched_waking",1,298,98,1
+          20,1735490042084,"sched_waking",1,298,102,1
         """))
 
   def test_thread_executing_span_graph(self):
@@ -677,13 +677,13 @@
         """))
 
   # Test the support of machine_id ID of the raw table.
-  def test_raw_machine_id(self):
+  def test_ftrace_event_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 LEFT JOIN cpu USING (ucpu)
+        FROM ftrace_event LEFT JOIN cpu USING (ucpu)
         WHERE machine_id is NULL;
         """,
         out=Csv("""
diff --git a/tools/bazel b/tools/bazel
new file mode 100755
index 0000000..5c618c7
--- /dev/null
+++ b/tools/bazel
@@ -0,0 +1,23 @@
+#!/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 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 os
+import sys
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(ROOT_DIR)
+__package__ = 'tools'
+from .run_buildtools_binary import run_buildtools_binary
+run_buildtools_binary(['bazel'] + sys.argv[1:])
diff --git a/tools/check_sql_metrics.py b/tools/check_sql_metrics.py
index 4d9810f..e56336c 100755
--- a/tools/check_sql_metrics.py
+++ b/tools/check_sql_metrics.py
@@ -66,6 +66,9 @@
         'android_blocking_calls_cuj_calls'
     ],
     ('/android'
+     '/android_blocking_calls_cuj_per_frame_metric.sql'): [
+        'android_cujs'],
+    ('/android'
      '/android_blocking_calls_unagg.sql'): [
         'filtered_processes_with_non_zero_blocking_calls', 'process_info',
         'android_blocking_calls_unagg_calls'
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 5339b16..e9e28c5 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -37,18 +37,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9041560,
+        9599720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
     'sha256':
-        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+        '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -58,11 +58,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8375512,
+        8920424,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
     'sha256':
-        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+        '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -72,11 +72,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9134136,
+        9920848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
     'sha256':
-        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+        '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -86,11 +86,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6753020,
+        7430084,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
     'sha256':
-        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+        '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8740064,
+        9479552,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
     'sha256':
-        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+        '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -114,55 +114,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6792280,
+        7329320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
     'sha256':
-        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+        '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        8677992,
+        9232824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
     'sha256':
-        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+        'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9503704,
+        10121840,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
     'sha256':
-        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+        '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8964488,
+        9554408,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
     'sha256':
-        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+        'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8763904,
+        9316864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
     'sha256':
-        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+        '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/gen_stdlib_docs_json.py b/tools/gen_stdlib_docs_json.py
index 27b2c5c..e08d31e 100755
--- a/tools/gen_stdlib_docs_json.py
+++ b/tools/gen_stdlib_docs_json.py
@@ -17,6 +17,7 @@
 import os
 import sys
 import json
+import re
 from collections import defaultdict
 from typing import Dict
 
@@ -29,6 +30,13 @@
 def _summary_desc(s: str) -> str:
   return s.split('. ')[0].replace('\n', ' ')
 
+def _long_type_to_table(s: str):
+    pattern = '(?:[A-Z]*)\(([a-z_]*).([a-z_]*)\)'
+    m = re.match(pattern, s)
+    if not m:
+      return (None, None)
+    g = m.groups()
+    return (g[0], g[1])
 
 def main():
   parser = argparse.ArgumentParser()
@@ -101,7 +109,9 @@
             'cols': [{
                 'name': col_name,
                 'type': col.long_type,
-                'desc': col.description
+                'desc': col.description,
+                'table': _long_type_to_table(col.long_type)[0],
+                'column': _long_type_to_table(col.long_type)[1],
             } for (col_name, col) in table.cols.items()]
         } for table in docs.table_views],
         'functions': [{
@@ -112,6 +122,8 @@
                 'name': arg_name,
                 'type': arg.long_type,
                 'desc': arg.description,
+                'table': _long_type_to_table(arg.long_type)[0],
+                'column': _long_type_to_table(arg.long_type)[1],
             } for (arg_name, arg) in function.args.items()],
             'return_type': function.return_type,
             'return_desc': function.return_desc,
@@ -127,10 +139,14 @@
                 'name': arg_name,
                 'type': arg.long_type,
                 'desc': arg.description,
+                'table': _long_type_to_table(arg.long_type)[0],
+                'column': _long_type_to_table(arg.long_type)[1],
             } for (arg_name, arg) in function.args.items()],
             'cols': [{
                 'name': col_name,
                 'type': col.long_type,
+                'table': _long_type_to_table(col.long_type)[0],
+                'column': _long_type_to_table(col.long_type)[1],
                 'desc': col.description
             } for (col_name, col) in function.cols.items()]
         } for function in docs.table_functions],
@@ -149,6 +165,8 @@
                 'name': arg_name,
                 'type': arg.long_type,
                 'desc': arg.description,
+                'table': _long_type_to_table(arg.long_type)[0],
+                'column': _long_type_to_table(arg.long_type)[1],
             } for (arg_name, arg) in macro.args.items()],
         } for macro in docs.macros],
     }
diff --git a/tools/gen_tp_table_docs.py b/tools/gen_tp_table_docs.py
index c69ab58..39f016a 100755
--- a/tools/gen_tp_table_docs.py
+++ b/tools/gen_tp_table_docs.py
@@ -40,7 +40,7 @@
   assert table.table.tabledoc
 
   # id and type columns should be skipped if the table specifies so.
-  is_skippable_col = col.is_implicit_id or col.is_implicit_type
+  is_skippable_col = col.is_implicit_id
   if table.table.tabledoc.skip_id_and_type and is_skippable_col:
     return None
 
diff --git a/tools/heap_profile b/tools/heap_profile
index df14a90..150bad2 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -34,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9041560,
+        9599720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
     'sha256':
-        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+        '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8375512,
+        8920424,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
     'sha256':
-        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+        '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9134136,
+        9920848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
     'sha256':
-        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+        '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6753020,
+        7430084,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
     'sha256':
-        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+        '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8740064,
+        9479552,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
     'sha256':
-        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+        '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,55 +111,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6792280,
+        7329320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
     'sha256':
-        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+        '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        8677992,
+        9232824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
     'sha256':
-        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+        'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9503704,
+        10121840,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
     'sha256':
-        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+        '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8964488,
+        9554408,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
     'sha256':
-        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+        'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8763904,
+        9316864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
     'sha256':
-        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+        '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/install-build-deps b/tools/install-build-deps
index a555d56..a29618b 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -330,6 +330,31 @@
             '0ce01e934f95efb6a216a6efa35af1245151c779', 'all', 'all'),
 ]
 
+# Dependencies required to build code on the host using Bazel build system.
+# Only macOS and Linux.
+BUILD_DEPS_BAZEL = [
+    Dependency(
+        'buildtools/mac/bazel',
+        'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-darwin-x86_64',
+        '52dd34c17cc97b3aa5bdfe3d45c4e3938226f23dd0bfb47beedd625a953f1f05',
+        'darwin', 'x64'),
+    Dependency(
+        'buildtools/mac/bazel',
+        'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-darwin-arm64',
+        '02b117b97d0921ae4d4f4e11d27e2c0930381df416e373435d5d0419c6a26f24',
+        'darwin', 'arm64'),
+    Dependency(
+        'buildtools/linux64/bazel',
+        'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-linux-x86_64',
+        'c97f02133adce63f0c28678ac1f21d65fa8255c80429b588aeeba8a1fac6202b',
+        'linux', 'x64'),
+    Dependency(
+        'buildtools/linux64/bazel',
+        'https://github.com/bazelbuild/bazel/releases/download/7.4.1/bazel-7.4.1-linux-arm64',
+        'd7aedc8565ed47b6231badb80b09f034e389c5f2b1c2ac2c55406f7c661d8b88',
+        'linux', 'arm64'),
+]
+
 # Dependencies required to build Android code.
 # URLs and SHA1s taken from:
 # - https://dl.google.com/android/repository/repository-11.xml
@@ -468,8 +493,8 @@
 ]
 
 ALL_DEPS = (
-    BUILD_DEPS_HOST + BUILD_DEPS_ANDROID + BUILD_DEPS_LINUX_CROSS_SYSROOTS +
-    TEST_DEPS_ANDROID + UI_DEPS)
+    BUILD_DEPS_HOST + BUILD_DEPS_BAZEL + BUILD_DEPS_ANDROID +
+    BUILD_DEPS_LINUX_CROSS_SYSROOTS + TEST_DEPS_ANDROID + UI_DEPS)
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 UI_DIR = os.path.join(ROOT_DIR, 'ui')
@@ -676,6 +701,10 @@
 def Main():
   parser = argparse.ArgumentParser()
   parser.add_argument(
+      '--bazel',
+      action='store_true',
+      help='Bazel build tool executable to build the project using Bazel')
+  parser.add_argument(
       '--android',
       action='store_true',
       help='NDK and emulator images target_os="android"')
@@ -720,6 +749,8 @@
   deps = BUILD_DEPS_HOST
   if not args.no_toolchain:
     deps += BUILD_DEPS_TOOLCHAIN_HOST
+  if args.bazel:
+    deps += BUILD_DEPS_BAZEL
   if args.android:
     deps += BUILD_DEPS_ANDROID + TEST_DEPS_ANDROID
   if args.linux_arm:
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 203b636..322fce8 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -34,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1613864,
+        1646808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
     'sha256':
-        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+        '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1492184,
+        1508856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
     'sha256':
-        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+        'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2380040,
+        2415168,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
     'sha256':
-        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+        '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1450708,
+        1478024,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
     'sha256':
-        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+        '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2269816,
+        2304384,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
     'sha256':
-        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+        '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,44 +111,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1333336,
+        1354916,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
     'sha256':
-        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+        '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2115984,
+        2142008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
     'sha256':
-        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+        '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2302960,
+        2341884,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
     'sha256':
-        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+        '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2147880,
+        2178416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
     'sha256':
-        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+        '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/trace_processor b/tools/trace_processor
index 29e6186..999589a 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9949656,
+        10524008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'e9dcf95aaa02f8c00a724f0ff34ba3a454c717beb9900cf9fd97ab142b362452',
+        '867c70800cfe81c2640f2aae8bb58eca68fa1389a3258a25c285ee5510edbbe3',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9223224,
+        9767976,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/trace_processor_shell',
     'sha256':
-        '9a0541a0f52f95bfcb8dc88d94bc4494c660d95eefc40fc946ab43d995051ff7',
+        '9c325030078bc4de8693083c9e4e2b72c83ca694c3a4ef8cc1bd9c29fb421815',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10142800,
+        10935344,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '18c8730b52f8ee1d9e202031527435b6b2e3149fbd9b1046b2e77d18f06aa337',
+        '6af6f87e6521eec186e74c68c0c6eeeeb557556e368d0e4f563be5ce5d9d936b',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7329432,
+        8010576,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/trace_processor_shell',
     'sha256':
-        '0558040998666576e1063d6d626b8aa9e354f18d73d225240f043b3c9236befa',
+        'da0c361d4a2c8d8b2d1ffd45cd388d964cc58b09e8e41f48aa045ed357510755',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9703384,
+        10448648,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/trace_processor_shell',
     'sha256':
-        'eeb95cc54358df08375ffae4862c043a6737902179ce8e0408984004c32cf93c',
+        'ecb6a1a073eb4bbfe36af56ab4406671e8febe02fb4c6dcef73fb1fe5d817fad',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        7367412,
+        7905948,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/trace_processor_shell',
     'sha256':
-        'd29b1e6aee52ceff24c072f56c7be7795d0fa29f3596e2633fafa60782384718'
+        'efeed53819e11f5f82d1ec9c3edce00590b3db1e3e4f0e64acddafba2c35a52e'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9598784,
+        10154712,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/trace_processor_shell',
     'sha256':
-        '06e80c562c0043cca9225ade3c961a081bcc7435660117d5a6db26b815d0b9ca'
+        'c6b0bb85228e4a3785030bbaf718bb428580f7d31de127e73042a30b9a67128a'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        10625488,
+        11244904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/trace_processor_shell',
     'sha256':
-        '2a576fb397da14d0dabcfa97f5eeec15b4dc55df009308f75a5fdf9de8a9b0dd'
+        'a113d0f68b89c63da4faefebc094a5be15620afdbe862a23127d55d7ff44ed43'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9915664,
+        10506544,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/trace_processor_shell',
     'sha256':
-        'a30be9f09b53110394e87af4d6b41ae24cd74d9a3f97ac1cc4d6ae2057ac6977'
+        'f062e38fd28ab94b0232df8f4eeac70506bb7d304b82100e288d5acb603f84c1'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        9922560,
+        10479616,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'd41639844a6c36dbaa195d91e9c356f2172d924c70a1bfed5432c407f857f009',
+        'a881f3e2d4c6131493e85bfd1f36d1efe58e1478e2991825418d5d21614c1e48',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index 9389051..ee19bf8 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1613864,
+        1646808,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
     'sha256':
-        'dfb1a3affe905d2e7d1f82bc4dda46b1fda6db054d60ae87c3215dd529b77fee',
+        '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1492184,
+        1508856,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
     'sha256':
-        '4a492a629dd1f13f3146c4b8267c0b163afba8cef1d49e0c00c48bb727496066',
+        'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2380040,
+        2415168,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
     'sha256':
-        'd70b284e8c28858fd539ae61ca59764d7f9fd6232073c304926e892fe75e692a',
+        '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1450708,
+        1478024,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
     'sha256':
-        '178fa6a1a9bc80f72d81938d40fe201c25c595ffaff7e030d59c2af09dfcc06c',
+        '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2269816,
+        2304384,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
     'sha256':
-        '42c64f9807756aaa08a2bfa13e9e4828c193a6b90ba1329408873c3ebf5adf3f',
+        '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,44 +107,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1333336,
+        1354916,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
     'sha256':
-        '93a78d2c42e3c00f117e2f155326383f69c891281ed693a39d87b8cb54ca4e19'
+        '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        2115984,
+        2142008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
     'sha256':
-        '508248a9e47ab605fd742efb700391d7267b68b586199a93e13e6ca14b72fe3d'
+        '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        2302960,
+        2341884,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
     'sha256':
-        '63d20a69c4e0c291329d7917e640fa0d4f146c344e79988e87393b1431d594b1'
+        '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2147880,
+        2178416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
     'sha256':
-        'c0ea1d5fd6d020e4c2b45d4d45cdd0c44ae63cd755d69260a3e5d2bacd3cbd6a'
+        '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index 55805b0..c69d6e4 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v48.1
+# This file has been generated by: tools/roll-prebuilts v49.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        9041560,
+        9599720,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/traceconv',
     'sha256':
-        'cec2da5cb771a4812d0b2d15604d5023954d28e0af12e87313da2ab70d26b970',
+        '5e583da4ee716b077a649f366049fbe1eed8ff8f469db92d841307eb817e06c7',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8375512,
+        8920424,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/traceconv',
     'sha256':
-        '64e200a58ea9c9f366e1071dd274d0023d1fd14043f75dbba3fe0cc138ff5fc7',
+        '794f45213cb81511c6e2594c47d917ce407650d81c16e2ff1442685e5da3a533',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        9134136,
+        9920848,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/traceconv',
     'sha256':
-        '87b87e1778367c1e3b99fc77439a28b4911125d2751f9909fd1b51f6bd60b6f4',
+        '5f0b86cfb8d75fd574aaabc36c97d229d5234511d3bf77ddcc2a180b96cbd014',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6753020,
+        7430084,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/traceconv',
     'sha256':
-        '804c4e13aca5798731056952d9cb0c6ee58795c03477c69514ccd39703060812',
+        '1561f9bbbd2b192b834132bd8c515cfde6f6afe2117bf68ac0aeb3caedfeb3fd',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8740064,
+        9479552,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/traceconv',
     'sha256':
-        '0d781886531d11e1d573a1ec5e06376ef139bb479eec38c16c8735821c35b895',
+        '4cb56805a5d1baf5756f459d5fa4a05c982faffc8fc96d9760ca3e86c6ced279',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6792280,
+        7329320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/traceconv',
     'sha256':
-        '7d91e4133184a3722a25488edd3692c5a195148eba56621014311d3f85d3fc15'
+        '719ac44e87c45a58d1ba3a6264518ed7384f738cdc293703b7e9a29ebcde6788'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        8677992,
+        9232824,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/traceconv',
     'sha256':
-        'c03c4a901ed23f1e20a12c98ce4556353a62bddcd260fb4d797cd29ff6c49a05'
+        'a76f954e8b6bba1e302ee136745ae5a478ba4737bf97bde1f8eeeec1b5238de2'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        9503704,
+        10121840,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/traceconv',
     'sha256':
-        '704e58a7249de56aadec64d4c0d83bab0821d2c4fd77114a9b71705ff4224539'
+        '7dcbe7ce3962155a156cb3e85e7fe17389973f93bf22b14ce13e45173f263ea4'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8964488,
+        9554408,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/traceconv',
     'sha256':
-        'e4f07836fc2a5fb7cd997a9acc4183af7a06997d1e73aac71021af5114b921bc'
+        'e44bc63def32674c99c67e5525ea36b66b6c1714e8fffe7606957813aaf212fa'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        8763904,
+        9316864,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v48.1/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/windows-amd64/traceconv.exe',
     'sha256':
-        '084670ac28ed59a9642782a30e051735c1b7474b8cd569b9bc94c305af68290e',
+        '937df755c7a54484c1a1aa1bbbaf392d978c300d6ca631bd9d9a20fe2b974deb',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/traced_perf.rc b/traced_perf.rc
index 7b0ec6a..16b44ae 100644
--- a/traced_perf.rc
+++ b/traced_perf.rc
@@ -28,6 +28,7 @@
     group nobody readproc readtracefs
     capabilities KILL DAC_READ_SEARCH
     task_profiles ProcessCapacityHigh
+    shared_kallsyms
 
 # Daemon run state:
 # * initially off
diff --git a/ui/build.js b/ui/build.js
index 0bf73f5..c44277a 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -120,7 +120,7 @@
   {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
   {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
   {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
-  {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
+  {r: /ui\/src\/assets\/.+[.]scss|ui\/src\/(?:plugins|core_plugins)\/.+\/styles[.]scss/, f: compileScss},
   {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
   {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
   {r: /.*\/dist\/.*[.](js|html|css|wasm)$/, f: notifyLiveServer},
@@ -251,6 +251,8 @@
     generateImports('ui/src/core_plugins', 'all_core_plugins');
     generateImports('ui/src/plugins', 'all_plugins');
     scanDir('ui/src/assets');
+    scanDir('ui/src/plugins', /styles[.]scss$/);
+    scanDir('ui/src/core_plugins', /styles[.]scss$/);
     scanDir('ui/src/chrome_extension');
     scanDir('buildtools/typefaces');
     scanDir('buildtools/catapult_trace_viewer');
diff --git a/ui/release/channels.json b/ui/release/channels.json
index f8af51d..0fb01f1 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "54a19ff810b506017d082f6ebfd9feac2885c558"
+      "rev": "6f67beebb7f4ae453b78b327e8ec4113851192c5"
     },
     {
       "name": "canary",
-      "rev": "94595b08a72b0338c55d947271a37829a0271729"
+      "rev": "e31359d73a91c34bebb86494dff0d67a18e8bbc4"
     },
     {
       "name": "autopush",
diff --git a/ui/src/assets/panel_container.scss b/ui/src/assets/panel_container.scss
deleted file mode 100644
index 0500294..0000000
--- a/ui/src/assets/panel_container.scss
+++ /dev/null
@@ -1,32 +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.
-
-.pf-panel-container {
-  // We need to drag over this element for various reasons, so just disable
-  // selection over the entire thing.
-  // TODO(stevegolton): If we enable this, we can get scrolling while dragging,
-  // so we might want to enable this here and disable selection in titles
-  // instead.
-  user-select: none;
-
-  .pf-panel-stack {
-    position: relative; // Position overlay relative to this element
-
-    .pf-overlay {
-      position: absolute;
-      inset: 0; // Shorthand for [top, left, right, bottom]: 0
-      pointer-events: none; // Make this overlay invisible to pointer events
-    }
-  }
-}
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index e8b636d..0e90b46 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -14,7 +14,6 @@
 
 @import "typefaces";
 @import "common";
-@import "panel_container";
 @import "viewer_page";
 @import "home_page";
 @import "query_page";
@@ -58,7 +57,7 @@
 @import "widgets/text_input";
 @import "widgets/text_paragraph";
 @import "widgets/timestamp";
-@import "widgets/track_widget";
+@import "widgets/track_shell";
 @import "widgets/tree";
 @import "widgets/treetable";
 @import "widgets/vega_view";
diff --git a/ui/src/assets/query_page.scss b/ui/src/assets/query_page.scss
index 2903240..18af8f7 100644
--- a/ui/src/assets/query_page.scss
+++ b/ui/src/assets/query_page.scss
@@ -16,6 +16,7 @@
   overflow-y: auto;
   overflow-x: hidden;
   .pf-editor {
+    contain: strict; // See b/388579546
     width: 100%;
     height: 0;
     min-height: 3rem;
diff --git a/ui/src/assets/record.scss b/ui/src/assets/record.scss
index 62ad2c8..c99e9c3 100644
--- a/ui/src/assets/record.scss
+++ b/ui/src/assets/record.scss
@@ -339,6 +339,31 @@
     color: #ee3326;
   }
 
+  .record-ctl {
+    margin: 0.5em;
+    padding: 0.5em;
+    background-color: #eee;
+    display: grid;
+    grid-template-columns: 24px auto 24px;
+    box-sizing: content-box;
+    height: 24px;
+    line-height: 24px;
+    .hidden {
+      visibility: hidden;
+    }
+    .record-target {
+      font-size: 13px;
+      color: #666;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .pf-button {
+      padding: 0;
+      margin: 0;
+      font-size: 20px;
+    }
+  }
+
   background-color: #fcfcfc;
   border-right: 1px solid #eee;
   padding-bottom: 1em;
@@ -348,6 +373,12 @@
     font-size: 14px;
     font-weight: 700;
     margin: 1em;
+
+    .pf-button {
+      float: right;
+      padding: 0;
+      font-size: 18px;
+    }
   }
 
   ul {
@@ -394,6 +425,12 @@
       display: block;
     }
 
+    .probe-count {
+      float: right;
+      font-size: 10px;
+      font-weight: 300;
+    }
+
     .sub {
       @include transition(0.5s);
       grid-area: subtext;
@@ -417,6 +454,11 @@
       }
     }
 
+    &.disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+
     &.active {
       background-color: hsl(214, 80%, 70%);
       .title,
@@ -485,6 +527,10 @@
       box-shadow: 0 0 3px 0 #aaa;
     }
 
+    &:active:enabled {
+      background-color: hsl(0, 0%, 83%);
+    }
+
     &:not(:enabled) {
       background-color: hsl(0, 0%, 83%);
       color: hsl(0, 0%, 50%);
@@ -701,7 +747,7 @@
       .probe-config {
         opacity: 1;
         visibility: visible;
-        max-height: 100vh;
+        max-height: 10000px;
       }
       > label span {
         color: #4e80b7;
@@ -805,7 +851,9 @@
     > * {
       @include transition(0.2s);
       cursor: pointer;
-      border-radius: 15px;
+      border-radius: 4px;
+      box-shadow: 0 0 4px 0 #ccc;
+
       margin: 5px;
       text-align: center;
       background-color: #eee;
@@ -817,13 +865,11 @@
       padding-bottom: 10px;
 
       &:hover {
-        background-color: hsl(88, 50%, 84%);
-        box-shadow: 0 0 4px 0 #999;
+        background-color: #c0def7;
       }
 
       &.selected {
-        background-color: hsl(88, 50%, 67%);
-        box-shadow: 0 0 4px 0 #999;
+        background-color: #64b5f6;
       }
 
       img {
@@ -1222,7 +1268,7 @@
     font-size: 14px;
   }
 
-  line-height: 25px;
+  display: inline-block;
   font-size: smaller;
   padding: 2px 4px;
   border: 1px solid #eee;
@@ -1241,7 +1287,7 @@
   display: grid;
   position: relative;
   padding: 0;
-  margin: var(--record-section-padding);
+  margin: 0 var(--record-section-padding);
   background-color: #111;
   border-radius: 4px;
   box-shadow: 0 0 12px #999;
@@ -1325,3 +1371,152 @@
     transform: scale(0.9);
   }
 } // code-snippet
+
+// For RecordingV2
+
+.record-section[id="target"] {
+  header {
+    font-size: 1rem;
+    font-weight: 600;
+    text-align: left;
+    padding-top: 20px;
+    padding-bottom: 5px;
+  }
+  .platform-selector button {
+    min-height: 60px;
+    box-shadow: 2px 2px 4px #eee;
+    border: 1px solid #eee;
+    font-size: 1.2rem;
+    margin: 0 5px;
+
+    &:hover {
+      background-color: #c0def7;
+    }
+    &.pf-active {
+      background-color: #64b5f6;
+    }
+  }
+
+  table {
+    width: 100%;
+  }
+
+  .record-transports {
+    border: 0;
+    input[type="radio"] + label {
+      cursor: pointer;
+      border: 1px solid #eee;
+      display: block;
+      &:hover .pf-icon {
+        background-color: #c0def7;
+      }
+
+      display: grid;
+      grid-template-columns: 50px auto;
+      grid-template-rows: auto auto;
+
+      grid-template-areas:
+        "icon title"
+        "icon description";
+      .pf-icon {
+        grid-area: icon;
+        font-size: 32px;
+        text-align: center;
+        padding: 10px 0;
+        margin-right: 10px;
+      }
+      .title {
+        grid-area: title;
+        font-weight: 600;
+        margin-top: 10px;
+      }
+      .description {
+        grid-area: description;
+        font-size: 0.75rem;
+        margin-bottom: 10px;
+      }
+    }
+    input[type="radio"]:checked + label {
+      .pf-icon {
+        background-color: #64b5f6;
+      }
+    }
+
+    input[type="radio"] {
+      -moz-appearance: none;
+      -webkit-appearance: none;
+      margin: 0;
+      padding: 0;
+      display: none;
+    }
+  } // .record-transports
+
+  .record-targets {
+    display: flex;
+    flex-direction: row;
+    margin: 10px 0;
+    > * {
+      margin-right: 10px;
+    }
+  }
+
+  .preflight-checks-icon {
+    .error {
+      color: hsl(-10, 50%, 50%);
+    }
+    .ok {
+      color: hsl(131, 50%, 50%);
+    }
+  }
+
+  .preflight-checks-table {
+    font-family: var(--monospace-font);
+    font-size: 0.8rem;
+    display: block;
+    border-collapse: collapse;
+
+    td {
+      padding: 5px 10px;
+      border-bottom: 1px solid #eee;
+      white-space: pre-wrap;
+    }
+    .error {
+      color: hsl(-10, 50%, 50%);
+      &::before {
+        content: "❌";
+        margin-right: 5px;
+      }
+    }
+    .ok {
+      color: hsl(131, 50%, 50%);
+      &::before {
+        content: "✅";
+        margin-right: 5px;
+      }
+    }
+  }
+
+  .session-status {
+    border: 1px solid #ddd;
+    margin: 1em;
+    padding: 1em;
+    width: calc(100% - 2em);
+    background-color: #fafafa;
+    font-size: 14px;
+    td:first-of-type {
+      font-weight: 500;
+      width: 20%;
+      vertical-align: top;
+    }
+    tr {
+      margin: 10px 0;
+    }
+    .logs {
+      background-color: rgba(0, 0, 0, 0.02);
+      border: 1px solid #eee;
+      white-space: pre-wrap;
+      margin: 0;
+      font-size: 12px;
+    }
+  }
+}
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
index 7a69912..b1847fe 100644
--- a/ui/src/assets/viewer_page.scss
+++ b/ui/src/assets/viewer_page.scss
@@ -12,102 +12,59 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-.viewer-page {
-  position: relative;
+.pf-viewer-page {
+  isolation: isolate;
   overflow: hidden;
 
-  .pinned-panel-container {
-    max-height: 50%;
-    box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
-    z-index: 1;
-    flex-grow: 0;
-    flex-shrink: 0;
-    overflow-x: hidden;
-    overflow-y: auto;
-    scrollbar-gutter: stable;
+  &__overview {
+    z-index: 3;
+    background: white;
+
+    .pf-overview-timeline {
+      height: 70px;
+    }
   }
 
-  .scrolling-panel-container {
-    overflow-x: hidden;
-    overflow-y: auto;
-    scrollbar-gutter: stable;
-    flex: 1 1 auto;
-    will-change: transform; // Force layer creation.
-  }
-
-  .pf-timeline-header {
-    display: flex;
-    flex-direction: row;
+  &__header {
+    display: grid;
+    grid-template-columns: 1fr auto;
     box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
     z-index: 2;
+    background: white;
 
-    .header-panel-container {
-      flex-grow: 1;
-    }
-
-    .scrollbar-spacer-vertical {
+    &:after {
+      content: "";
       scrollbar-gutter: stable;
       overflow-y: scroll;
       visibility: hidden;
+      flex: 0 0 auto;
     }
   }
 
-  .pan-and-zoom-content {
-    height: 100%;
-    position: relative;
-    display: flex;
-    flex-flow: column nowrap;
-    overflow: hidden;
-  }
-
-  .overview-timeline {
-    height: 70px;
-  }
-
-  .time-axis-panel {
-    height: 22px;
-  }
-
-  .tickbar {
-    height: 5px;
-  }
-
-  .notes-panel {
-    height: 20px;
-    .pf-toolbar {
-      width: var(--track-shell-width);
-      padding-inline: 3px;
-
-      > .pf-text-input {
-        flex: auto;
-      }
-    }
-  }
-
-  .time-selection-panel {
-    height: 10px;
-  }
-
-  .helpful-hint {
-    position: absolute;
-    z-index: 10;
-    right: 5px;
-    top: 5px;
-    width: 300px;
-    background-color: white;
-    font-size: 12px;
-    color: #3f4040;
-    display: grid;
-    border-radius: 5px;
-    padding: 8px;
+  &__pinned-track-tree {
+    flex: 0 0 auto;
+    max-height: 40%;
+    z-index: 1;
     box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
+    scrollbar-gutter: stable;
+  }
+
+  &__scrolling-track-tree {
+    flex: 1 1 auto;
+    scrollbar-gutter: stable;
   }
 }
 
-.pf-track-crash-popup {
-  font-family: $pf-font;
-  max-width: 300px;
-  display: flex;
-  flex-direction: column;
-  row-gap: 6px;
+.pf-timeline-toolbar {
+  width: var(--track-shell-width);
+  border-right: solid 1px transparent;
+  padding-inline: 2px;
+
+  &__workspace-selector {
+    flex: 1;
+  }
+}
+
+.pf-track__track-details-popup {
+  min-width: 350px;
 }
diff --git a/ui/src/assets/widgets/track_shell.scss b/ui/src/assets/widgets/track_shell.scss
new file mode 100644
index 0000000..e4c1929
--- /dev/null
+++ b/ui/src/assets/widgets/track_shell.scss
@@ -0,0 +1,196 @@
+// 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";
+
+.pf-track {
+  --text-color-dark: hsl(213, 22%, 30%);
+  --text-color-light: white;
+  --drag-highlight-color: rgb(29, 85, 189);
+  --default-background: white;
+  --collapsed-background: hsla(190, 49%, 97%, 1);
+  --expanded-background: hsl(215, 22%, 19%);
+  --indent-size: 8px;
+
+  // Default values - should be overridden from JS
+  --sticky-top: 0;
+  --depth: 0;
+
+  font-family: "Roboto Condensed", sans-serif;
+  font-weight: 300;
+  font-size: 14px;
+  color: var(--text-color-dark);
+  background-color: var(--default-background);
+  user-select: none;
+
+  &__header {
+    height: calc(var(--height) * 1px);
+    display: grid;
+    grid-template-columns:
+      calc(var(--depth) * var(--indent-size)) calc(
+        var(--track-shell-width) - (var(--depth) * var(--indent-size))
+      )
+      1fr;
+    grid-template-areas: "indent shell canvas";
+
+    &::before {
+      content: "";
+      grid-area: indent;
+      background: lightgray;
+    }
+
+    &--summary {
+      background-color: var(--collapsed-background);
+    }
+
+    &--expanded--summary {
+      position: sticky;
+      top: calc(var(--sticky-top) * 1px);
+      z-index: calc(16 - var(--depth));
+      background-color: var(--expanded-background);
+      color: var(--text-color-light);
+    }
+  }
+
+  &__shell {
+    grid-area: shell;
+    border-width: 0 1px 1px 0; // Borders on bottom and right
+    border-style: solid;
+    border-color: var(--track-border-color);
+
+    &--highlight {
+      background-color: #ffe263;
+      color: var(--text-color-dark);
+    }
+
+    &--drag-after {
+      box-shadow: inset 0 -6px 3px -3px var(--drag-highlight-color);
+    }
+
+    &--drag-before {
+      box-shadow: inset 0 6px 3px -3px var(--drag-highlight-color);
+    }
+
+    &--clickable {
+      cursor: pointer;
+    }
+
+    .pf-visible-on-hover {
+      visibility: hidden;
+
+      &.pf-active {
+        visibility: unset;
+      }
+    }
+
+    &:hover {
+      .pf-visible-on-hover {
+        visibility: unset;
+      }
+    }
+  }
+
+  &__buttons {
+    // Make the track buttons a little larger so they're easier to see & click
+    font-size: 16px;
+    margin-left: 2px;
+  }
+
+  &__canvas {
+    grid-area: canvas;
+    overflow: hidden; // Keeps subtitle width contained
+    border-bottom: solid 1px var(--track-border-color);
+
+    &--error {
+      // Necessary trig because we have 45deg stripes
+      $pattern-density: 1px * math.sqrt(2);
+      $pattern-col: #ddd;
+
+      background: repeating-linear-gradient(
+        -45deg,
+        $pattern-col,
+        $pattern-col $pattern-density,
+        white $pattern-density,
+        white $pattern-density * 4
+      );
+    }
+  }
+
+  &__menubar {
+    display: grid;
+    grid-template-columns: auto 1fr auto auto;
+    grid-template-rows: auto auto;
+    grid-template-areas:
+      "icon title chips buttons"
+      "none subtitle subtitle subtitle";
+    padding: 1px;
+    position: sticky;
+    top: calc(var(--sticky-top) * 1px);
+  }
+
+  &__title-spacer {
+    grid-area: icon;
+    width: 2px;
+  }
+
+  &__collapse-button {
+    grid-area: icon;
+  }
+
+  &__title {
+    grid-area: title;
+
+    &-popup {
+      position: absolute;
+      border-radius: 2px;
+      color: rgba(0, 0, 0, 0);
+      background: rgba(255, 255, 255, 0);
+      text-overflow: unset;
+      pointer-events: none;
+      white-space: nowrap;
+      user-select: none;
+      width: max-content;
+      z-index: 10; /* Ensures it appears above other elements */
+    }
+
+    &:hover &-popup--visible {
+      box-shadow: 1px 1px 2px 2px var(--track-border-color);
+      background: white;
+      color: hsl(213, 22%, 30%);
+    }
+  }
+
+  &__chips {
+    grid-area: chips;
+    margin-left: 2px;
+  }
+
+  &__subtitle {
+    grid-area: subtitle;
+    font-size: 11px;
+  }
+
+  &__buttons {
+    grid-area: buttons;
+  }
+
+  &__crash-popup {
+    font-family: $pf-font;
+    max-width: 300px;
+    display: flex;
+    flex-direction: column;
+    row-gap: 6px;
+  }
+}
diff --git a/ui/src/assets/widgets/track_widget.scss b/ui/src/assets/widgets/track_widget.scss
deleted file mode 100644
index ed9359d..0000000
--- a/ui/src/assets/widgets/track_widget.scss
+++ /dev/null
@@ -1,180 +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.
-
-@use "sass:math";
-@import "theme";
-
-.pf-track {
-  --text-color-dark: hsl(213, 22%, 30%);
-  --text-color-light: white;
-  --indent-size: 8px;
-  --drag-highlight-color: rgb(29, 85, 189);
-  --default-background: white;
-  --collapsed-background: hsla(190, 49%, 97%, 1);
-  --expanded-background: hsl(215, 22%, 19%);
-
-  // Layout
-  display: grid;
-  grid-template-columns:
-    calc(var(--indent) * var(--indent-size)) calc(
-      250px - (var(--indent) * var(--indent-size))
-    )
-    1fr;
-  grid-template-areas: "indent shell content";
-
-  // Appearance
-  font-family: "Roboto Condensed", sans-serif;
-  font-weight: 300;
-  font-size: 14px;
-  color: var(--text-color-dark);
-  background-color: var(--default-background);
-
-  &::before {
-    content: "";
-    grid-area: indent;
-    background: lightgray;
-    display: block;
-    height: 100%;
-  }
-
-  .pf-track-shell {
-    grid-area: shell;
-    background-color: inherit;
-    box-shadow: inset 0px -1px 0px 0px var(--track-border-color);
-
-    &.pf-drag-after {
-      box-shadow: inset 0 -6px 3px -3px var(--drag-highlight-color);
-    }
-
-    &.pf-drag-before {
-      box-shadow: inset 0 6px 3px -3px var(--drag-highlight-color);
-    }
-
-    &.pf-clickable {
-      cursor: pointer;
-    }
-
-    .pf-visible-on-hover {
-      visibility: hidden;
-
-      &.pf-active {
-        visibility: unset;
-      }
-    }
-
-    &:hover {
-      .pf-visible-on-hover {
-        visibility: unset;
-      }
-    }
-
-    .pf-track-menubar {
-      display: grid;
-      grid-template-columns: auto 1fr auto; // title, buttons
-      grid-template-rows: auto auto;
-      grid-template-areas:
-        "icon title buttons"
-        "none subtitle subtitle";
-      padding: 1px 2px;
-      column-gap: 2px;
-
-      h1.pf-track-title {
-        grid-area: title;
-
-        // Override h1 formatting
-        font-size: inherit;
-        margin: inherit;
-
-        display: flex;
-        flex-direction: row;
-        gap: 2px;
-        overflow: hidden;
-
-        .pf-track__title-popup {
-          border-radius: 2px;
-          color: rgba(0, 0, 0, 0);
-          background: rgba(255, 255, 255, 0);
-          position: absolute;
-          text-overflow: unset;
-          pointer-events: none;
-          white-space: nowrap;
-          user-select: none;
-        }
-
-        &:hover .pf-track__title-popup.pf-visible {
-          box-shadow: 1px 1px 2px 2px var(--track-border-color);
-          background: white;
-          color: hsl(213, 22%, 30%);
-        }
-      }
-
-      h2 {
-        // Override h2 formatting
-        margin: inherit;
-
-        grid-area: subtitle;
-        font-size: 11px;
-      }
-
-      .pf-track-buttons {
-        grid-area: buttons;
-        // Make the track buttons a little larger so they're easier to see &
-        // click
-        font-size: 16px;
-      }
-    }
-  }
-
-  &.pf-is-summary {
-    background-color: var(--collapsed-background);
-
-    &.pf-expanded {
-      background-color: var(--expanded-background);
-      color: var(--text-color-light);
-    }
-  }
-
-  &.pf-highlight {
-    .pf-track-shell {
-      background-color: #ffe263;
-      color: var(--text-color-dark);
-    }
-  }
-
-  .pf-track-content {
-    grid-area: content;
-    box-shadow: inset 1px -1px 0px 0px var(--track-border-color);
-    padding-inline: 2px;
-    overflow: hidden;
-
-    h2 {
-      margin: inherit;
-      font-size: inherit;
-    }
-
-    &.pf-track-content-error {
-      // Necessary trig because we have 45deg stripes
-      $pattern-density: 1px * math.sqrt(2);
-      $pattern-col: #ddd;
-
-      background: repeating-linear-gradient(
-        -45deg,
-        $pattern-col,
-        $pattern-col $pattern-density,
-        white $pattern-density,
-        white $pattern-density * 4
-      );
-    }
-  }
-}
diff --git a/ui/src/base/async_guard.ts b/ui/src/base/async_guard.ts
new file mode 100644
index 0000000..94cba84
--- /dev/null
+++ b/ui/src/base/async_guard.ts
@@ -0,0 +1,71 @@
+// 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.
+
+/**
+ * AsyncGuard<T> ensures that a given asynchronous operation does not overlap
+ * with itself.
+ *
+ * This class is useful in scenarios where you want to prevent concurrent
+ * executions of the same async function. If the function is already in
+ * progress, any subsequent calls to `run` will return the same promise,
+ * ensuring no new execution starts until the ongoing one completes.
+ *
+ * - Guarantees single execution: Only one instance of the provided async
+ *   function will execute at a time.
+ * - Automatically resets: Once the function completes (either successfully
+ *   or with an error), the guard resets and allows new executions.
+ *
+ * This class differs from AsyncLimiter in the fact that it has no queueing at
+ * all (AsyncLimiter instead keeps a queue of max_depth=1 and keeps over-writing
+ * the last task).
+ *
+ * Example:
+ * ```typescript
+ * const asyncTask = async () => {
+ *   console.log("Task started...");
+ *   await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work.
+ *   console.log("Task finished!");
+ *   return "Result";
+ * };
+ *
+ * const guard = new AsyncGuard<string>();
+ *
+ * // Simultaneous calls
+ * guard.run(asyncTask).then(console.log); // Logs "Task started..." and
+ *                                         // "Task finished!" -> "Result"
+ * guard.run(asyncTask).then(console.log); // Will not log "Task started..."
+ *                                         // again, reuses the promise
+ * ```
+ */
+export class AsyncGuard<T> {
+  private pendingPromise?: Promise<T>;
+
+  /**
+   * Runs the provided async function, ensuring no overlap.
+   * If a previous call is still pending, it returns the same promise.
+   *
+   * @param func - The async function to execute.
+   * @returns A promise resolving to the function's result.
+   */
+  run(func: () => Promise<T>): Promise<T> {
+    if (this.pendingPromise !== undefined) {
+      return this.pendingPromise;
+    }
+    this.pendingPromise = func();
+    this.pendingPromise.finally(() => {
+      this.pendingPromise = undefined;
+    });
+    return this.pendingPromise;
+  }
+}
diff --git a/ui/src/base/async_guard_unittest.ts b/ui/src/base/async_guard_unittest.ts
new file mode 100644
index 0000000..2e5c474
--- /dev/null
+++ b/ui/src/base/async_guard_unittest.ts
@@ -0,0 +1,37 @@
+// 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 {AsyncGuard} from './async_guard';
+import {defer} from './deferred';
+
+test('AsyncGuard', async () => {
+  const guard = new AsyncGuard<number>();
+  let counter = 0;
+
+  for (let i = 1; i <= 3; i++) {
+    const barrier = defer<void>();
+    const asyncTask = async () => {
+      await barrier;
+      return ++counter;
+    };
+    const promises = [
+      guard.run(asyncTask),
+      guard.run(asyncTask),
+      guard.run(asyncTask),
+    ];
+    setTimeout(() => barrier.resolve(), 0);
+    await barrier;
+    expect(await Promise.all(promises)).toEqual([i, i, i]);
+  }
+});
diff --git a/ui/src/base/async_lazy.ts b/ui/src/base/async_lazy.ts
new file mode 100644
index 0000000..1636e8d
--- /dev/null
+++ b/ui/src/base/async_lazy.ts
@@ -0,0 +1,55 @@
+// 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 {AsyncGuard} from './async_guard';
+import {okResult, Result} from './result';
+
+/**
+ * A utility class for lazily initializing and caching asynchronous values.
+ *
+ * This class ensures that a value is created only once using a provided
+ * asynchronous factory function and is cached for future access. It also
+ * provides methods to reset the cached value and retry the initialization.
+ *
+ * Internally, the class uses {@link AsyncGuard} to ensure non-overlapping of
+ * the initialization process, preventing race conditions when multiple
+ * callers attempt to initialize the value concurrently.
+ */
+export class AsyncLazy<T> {
+  private _value?: T;
+  private guard = new AsyncGuard<Result<T>>();
+
+  getOrCreate(factory: () => Promise<Result<T>>): Promise<Result<T>> {
+    if (this._value !== undefined) {
+      return Promise.resolve(okResult(this._value));
+    }
+
+    const promise = this.guard.run(factory);
+    promise.then((valueOrError) => {
+      if (valueOrError.ok) {
+        this._value = valueOrError.value;
+      }
+    });
+    return promise;
+  }
+
+  get value(): T | undefined {
+    return this._value;
+  }
+
+  reset() {
+    this._value = undefined;
+    this.guard = new AsyncGuard<Result<T>>();
+  }
+}
diff --git a/ui/src/base/async_lazy_unittest.ts b/ui/src/base/async_lazy_unittest.ts
new file mode 100644
index 0000000..66c7e2f
--- /dev/null
+++ b/ui/src/base/async_lazy_unittest.ts
@@ -0,0 +1,55 @@
+// 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 {AsyncLazy} from './async_lazy';
+import {defer} from './deferred';
+import {errResult, okResult, Result} from './result';
+
+async function slowFactory(res: number): Promise<Result<number>> {
+  const barrier = defer<void>();
+  setTimeout(() => barrier.resolve(), 0);
+  await barrier;
+  return isFinite(res) ? okResult(res) : errResult(`${res} is not a number`);
+}
+
+test('AsyncLazy', async () => {
+  const alazy = new AsyncLazy<number>();
+  expect(alazy.value).toBeUndefined();
+
+  // Failures during creation should not be cached.
+  expect(await alazy.getOrCreate(() => slowFactory(NaN))).toEqual(
+    errResult('NaN is not a number'),
+  );
+  expect(await alazy.getOrCreate(() => slowFactory(1 / 0))).toEqual(
+    errResult('Infinity is not a number'),
+  );
+
+  const promises = [
+    alazy.getOrCreate(() => slowFactory(42)),
+    alazy.getOrCreate(() => slowFactory(1)),
+    alazy.getOrCreate(() => slowFactory(2)),
+  ];
+
+  // Only the first promise will determine the result, which will be
+  // subsequently cached.
+  expect(await Promise.all(promises)).toEqual([
+    okResult(42),
+    okResult(42),
+    okResult(42),
+  ]);
+  expect(alazy.value).toEqual(42);
+
+  alazy.reset();
+  expect(await alazy.getOrCreate(() => slowFactory(99))).toEqual(okResult(99));
+});
diff --git a/ui/src/base/canvas/bezier_arrow.ts b/ui/src/base/bezier_arrow.ts
similarity index 98%
rename from ui/src/base/canvas/bezier_arrow.ts
rename to ui/src/base/bezier_arrow.ts
index c5f0a60..3ff04fd 100644
--- a/ui/src/base/canvas/bezier_arrow.ts
+++ b/ui/src/base/bezier_arrow.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Point2D, Vector2D} from '../geom';
-import {assertUnreachable} from '../logging';
+import {Point2D, Vector2D} from './geom';
+import {assertUnreachable} from './logging';
 
 export type CardinalDirection = 'north' | 'south' | 'east' | 'west';
 
diff --git a/ui/src/public/color.ts b/ui/src/base/color.ts
similarity index 99%
rename from ui/src/public/color.ts
rename to ui/src/base/color.ts
index f9e1430..4518e60 100644
--- a/ui/src/public/color.ts
+++ b/ui/src/base/color.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {hsluvToRgb} from 'hsluv';
-import {clamp} from '../base/math_utils';
+import {clamp} from './math_utils';
 
 // This file contains a library for working with colors in various color spaces
 // and formats.
diff --git a/ui/src/public/color_scheme.ts b/ui/src/base/color_scheme.ts
similarity index 100%
rename from ui/src/public/color_scheme.ts
rename to ui/src/base/color_scheme.ts
diff --git a/ui/src/base/gcs_uploader.ts b/ui/src/base/gcs_uploader.ts
index b2f2bd5..3ecbece 100644
--- a/ui/src/base/gcs_uploader.ts
+++ b/ui/src/base/gcs_uploader.ts
@@ -62,7 +62,7 @@
     this.start(data);
   }
 
-  async start(data: Blob | ArrayBuffer | string) {
+  private async start(data: Blob | ArrayBuffer | string) {
     let fname = this.args.fileName;
     if (fname === undefined) {
       // If the file name is unspecified, hash the contents.
diff --git a/ui/src/base/geom.ts b/ui/src/base/geom.ts
index 385bb22..c766fdb 100644
--- a/ui/src/base/geom.ts
+++ b/ui/src/base/geom.ts
@@ -298,4 +298,13 @@
       right: this.right + point.x,
     });
   }
+
+  equals(bounds: Bounds2D): boolean {
+    return (
+      bounds.top === this.top &&
+      bounds.left === this.left &&
+      bounds.right === this.right &&
+      bounds.bottom === this.bottom
+    );
+  }
 }
diff --git a/ui/src/base/high_precision_time.ts b/ui/src/base/high_precision_time.ts
index f205c83..2fce5e5 100644
--- a/ui/src/base/high_precision_time.ts
+++ b/ui/src/base/high_precision_time.ts
@@ -47,6 +47,17 @@
     this.fractional = fractional - fractionalFloor;
   }
 
+  static max(a: HighPrecisionTime, b: HighPrecisionTime) {
+    if (a.integral > b.integral) return a;
+    if (a.integral < b.integral) return b;
+    if (a.fractional > b.fractional) return a;
+    return b;
+  }
+
+  static min(a: HighPrecisionTime, b: HighPrecisionTime) {
+    return HighPrecisionTime.max(a, b) === a ? b : a;
+  }
+
   /**
    * Converts to an integer time value.
    *
diff --git a/ui/src/base/high_precision_time_span.ts b/ui/src/base/high_precision_time_span.ts
index 0bb6e75..f31a706 100644
--- a/ui/src/base/high_precision_time_span.ts
+++ b/ui/src/base/high_precision_time_span.ts
@@ -31,6 +31,19 @@
   }
 
   /**
+   * Create a new span from two high precision times. The earlier time is used
+   * as the start and and the later time as the end of the span.
+   */
+  static fromHpTimes(
+    a: HighPrecisionTime,
+    b: HighPrecisionTime,
+  ): HighPrecisionTimeSpan {
+    const start = HighPrecisionTime.min(a, b);
+    const end = HighPrecisionTime.max(a, b);
+    return new HighPrecisionTimeSpan(start, Number(end.sub(start)));
+  }
+
+  /**
    * Create a new span from integral start and end points.
    *
    * @param start The start of the span.
diff --git a/ui/src/base/high_precision_time_span_unittest.ts b/ui/src/base/high_precision_time_span_unittest.ts
index 537d173..8e717d4 100644
--- a/ui/src/base/high_precision_time_span_unittest.ts
+++ b/ui/src/base/high_precision_time_span_unittest.ts
@@ -49,6 +49,20 @@
     expect(span.duration).toBeCloseTo(10);
   });
 
+  it('can be constructed from hp times', () => {
+    const span = HPTimeSpan.fromHpTimes(hptime('123'), hptime('456'));
+    expect(span.start.integral).toEqual(123n);
+    expect(span.start.fractional).toBeCloseTo(0);
+    expect(span.duration).toBeCloseTo(333);
+  });
+
+  it('can be constructed from hp times reversed', () => {
+    const span = HPTimeSpan.fromHpTimes(hptime('456'), hptime('123'));
+    expect(span.start.integral).toEqual(123n);
+    expect(span.start.fractional).toBeCloseTo(0);
+    expect(span.duration).toBeCloseTo(333);
+  });
+
   test('end', () => {
     const span = HPTimeSpan.fromTime(t(10n), t(20n));
     expect(span.end.integral).toEqual(20n);
diff --git a/ui/src/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index 9b0615d..0bfcc7c 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -85,3 +85,5 @@
     },
   };
 }
+
+export type MithrilEvent<T extends Event = Event> = T & {redraw: boolean};
diff --git a/ui/src/base/resizable_array_buffer.ts b/ui/src/base/resizable_array_buffer.ts
new file mode 100644
index 0000000..a882c35
--- /dev/null
+++ b/ui/src/base/resizable_array_buffer.ts
@@ -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.
+
+import {assertTrue} from './logging';
+
+/**
+ * A dynamically resizable array buffer implementation for efficient
+ * storage and manipulation of binary data. It starts with a specified
+ * initial size and grows as needed to accommodate appended data.
+ * Efficiently grows the buffer using an exponential strategy up to 32MB,
+ * and then linearly in 32MB increments to minimize reallocation overhead.
+ * Provides methods to append data, shrink the size, clear the buffer,
+ * and retrieve the stored data as a `Uint8Array`.
+ */
+export class ResizableArrayBuffer {
+  private buf: Uint8Array;
+  private _size = 0;
+
+  constructor(private readonly initialSize = 128) {
+    this.buf = new Uint8Array(initialSize);
+  }
+
+  append(data: ArrayLike<number>) {
+    const capacityNeeded = this._size + data.length;
+    if (this.capacity < capacityNeeded) {
+      this.grow(capacityNeeded);
+    }
+    this.buf.set(data, this._size);
+    this._size = capacityNeeded;
+  }
+
+  shrink(newSize: number) {
+    assertTrue(newSize <= this._size);
+    this._size = newSize;
+  }
+
+  clear() {
+    this.buf = new Uint8Array(this.initialSize);
+    this._size = 0;
+  }
+
+  get(): Uint8Array {
+    return this.buf.subarray(0, this._size);
+  }
+
+  get size(): number {
+    return this._size;
+  }
+
+  get capacity(): number {
+    return this.buf.length;
+  }
+
+  private grow(capacityNeeded: number) {
+    let newSize = this.buf.length;
+    const MB32 = 32 * 1024 * 1024;
+    do {
+      newSize = newSize < MB32 ? newSize * 2 : newSize + MB32;
+    } while (newSize < capacityNeeded);
+    const newBuf = new Uint8Array(newSize);
+    assertTrue(newBuf.length >= capacityNeeded);
+    newBuf.set(this.buf);
+    this.buf = newBuf;
+  }
+}
diff --git a/ui/src/base/resizable_array_buffer_unittest.ts b/ui/src/base/resizable_array_buffer_unittest.ts
new file mode 100644
index 0000000..42adab3
--- /dev/null
+++ b/ui/src/base/resizable_array_buffer_unittest.ts
@@ -0,0 +1,55 @@
+// Copyright (C) 2018 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 {pseudoRand} from './rand';
+import {ResizableArrayBuffer} from './resizable_array_buffer';
+
+test('ResizableArrayBuffer.simple', () => {
+  const buf = new ResizableArrayBuffer();
+  buf.append([1, 2]);
+  expect(buf.get()).toEqual(new Uint8Array([1, 2]));
+
+  buf.append([3]);
+  expect(buf.get()).toEqual(new Uint8Array([1, 2, 3]));
+
+  buf.clear();
+  expect(buf.get().length).toEqual(0);
+  expect(buf.get()).toEqual(new Uint8Array([]));
+});
+
+test('ResizableArrayBuffer.merge', () => {
+  const buf = new ResizableArrayBuffer();
+
+  // Append to the buffer in batches of variables sizes, filling with a
+  // pseudo-random sequence.
+  const randState = {seed: 1};
+  const randU8 = () => (pseudoRand(randState) * 256) & 0xff;
+  const BATCH_SIZES = [37, 129, 1023, 1024 * 37, 1024 * 511];
+  for (const batchSize of BATCH_SIZES) {
+    const batch = new Uint8Array(batchSize);
+    for (let i = 0; i < batch.length; i++) {
+      batch[i] = randU8();
+    }
+    buf.append(batch);
+  }
+
+  // Now reset the seed and check that the random sequence of the merged array
+  // matches.
+  const merged = buf.get();
+  expect(merged.length).toEqual(BATCH_SIZES.reduce((a, s) => a + s, 0));
+  randState.seed = 1;
+  for (let i = 0; i < merged.length; i++) {
+    expect(merged[i]).toEqual(randU8());
+  }
+});
diff --git a/ui/src/base/result.ts b/ui/src/base/result.ts
index 9dd8cee..5b04a83 100644
--- a/ui/src/base/result.ts
+++ b/ui/src/base/result.ts
@@ -17,6 +17,7 @@
 export interface ErrorResult {
   ok: false;
   error: string;
+  value: undefined;
 }
 
 export interface OkResult<T> {
@@ -27,7 +28,7 @@
 export type Result<T> = ErrorResult | OkResult<T>;
 
 export function errResult(message: string): ErrorResult {
-  return {ok: false, error: message};
+  return {ok: false, error: message, value: undefined};
 }
 
 export function okResult<T>(value: T): OkResult<T> {
diff --git a/ui/src/base/string_utils.ts b/ui/src/base/string_utils.ts
index df0e3da..ad71674 100644
--- a/ui/src/base/string_utils.ts
+++ b/ui/src/base/string_utils.ts
@@ -127,3 +127,10 @@
   }
   return displayText;
 }
+
+export function splitLinesNonEmpty(text: string): string[] {
+  return text
+    .split('\n')
+    .map((l) => l.trim())
+    .filter((l) => l !== '');
+}
diff --git a/ui/src/base/string_utils_unittest.ts b/ui/src/base/string_utils_unittest.ts
index 96f63dd..61e6bf6 100644
--- a/ui/src/base/string_utils_unittest.ts
+++ b/ui/src/base/string_utils_unittest.ts
@@ -20,6 +20,7 @@
   binaryEncode,
   cropText,
   sqliteString,
+  splitLinesNonEmpty,
   utf8Decode,
   utf8Encode,
 } from './string_utils';
@@ -94,3 +95,14 @@
   expect(cropText(emoji + 'abc', 5, 2 * 5)).toBe(emoji);
   expect(cropText(emoji + 'abc', 5, 5 * 5)).toBe(emoji + 'a' + tripleDot);
 });
+
+test('string_utils.splitNonEmptyLines', () => {
+  expect(splitLinesNonEmpty('')).toEqual([]);
+  expect(splitLinesNonEmpty('foo')).toEqual(['foo']);
+  expect(splitLinesNonEmpty('foo  ')).toEqual(['foo']);
+  expect(splitLinesNonEmpty('foo\n')).toEqual(['foo']);
+  expect(splitLinesNonEmpty('foo\nbar')).toEqual(['foo', 'bar']);
+  expect(splitLinesNonEmpty('foo\nbar\n')).toEqual(['foo', 'bar']);
+  expect(splitLinesNonEmpty('foo\nbar\n')).toEqual(['foo', 'bar']);
+  expect(splitLinesNonEmpty('foo\n \nbar\n')).toEqual(['foo', 'bar']);
+});
diff --git a/ui/src/base/zoned_interaction_handler.ts b/ui/src/base/zoned_interaction_handler.ts
index 1995103..257c228 100644
--- a/ui/src/base/zoned_interaction_handler.ts
+++ b/ui/src/base/zoned_interaction_handler.ts
@@ -91,6 +91,8 @@
   // Default: 0 - drags start instantly.
   readonly minDistance?: number;
 
+  onDragStart?(e: DragEvent, element: HTMLElement): void;
+
   // Optional: Called whenever the mouse is moved during a drag event.
   onDrag?(e: DragEvent, element: HTMLElement): void;
 
diff --git a/ui/src/bigtrace/index.ts b/ui/src/bigtrace/index.ts
index db3e71b..f872585 100644
--- a/ui/src/bigtrace/index.ts
+++ b/ui/src/bigtrace/index.ts
@@ -19,7 +19,6 @@
 import {reportError, addErrorHandler, ErrorDetails} from '../base/logging';
 import {initLiveReloadIfLocalhost} from '../core/live_reload';
 import {raf} from '../core/raf_scheduler';
-import {setScheduleFullRedraw} from '../widgets/raf';
 
 function getRoot() {
   // Works out the root directory where the content should be served from
@@ -39,24 +38,12 @@
 function setupContentSecurityPolicy() {
   // Note: self and sha-xxx must be quoted, urls data: and blob: must not.
   const policy = {
-    'default-src': [
-      `'self'`,
-    ],
-    'script-src': [
-      `'self'`,
-    ],
+    'default-src': [`'self'`],
+    'script-src': [`'self'`],
     'object-src': ['none'],
-    'connect-src': [
-      `'self'`,
-    ],
-    'img-src': [
-      `'self'`,
-      'data:',
-      'blob:',
-    ],
-    'style-src': [
-      `'self'`,
-    ],
+    'connect-src': [`'self'`],
+    'img-src': [`'self'`, 'data:', 'blob:'],
+    'style-src': [`'self'`],
     'navigate-to': ['https://*.perfetto.dev', 'self'],
   };
   const meta = document.createElement('meta');
@@ -70,9 +57,6 @@
 }
 
 function main() {
-  // Wire up raf for widgets.
-  setScheduleFullRedraw(() => raf.scheduleFullRedraw());
-
   setupContentSecurityPolicy();
 
   // Load the css. The load is asynchronous and the CSS is not ready by the time
@@ -95,9 +79,13 @@
   window.addEventListener('unhandledrejection', (e) => reportError(e));
 
   // Prevent pinch zoom.
-  document.body.addEventListener('wheel', (e: MouseEvent) => {
-    if (e.ctrlKey) e.preventDefault();
-  }, {passive: false});
+  document.body.addEventListener(
+    'wheel',
+    (e: MouseEvent) => {
+      if (e.ctrlKey) e.preventDefault();
+    },
+    {passive: false},
+  );
 
   cssLoadPromise.then(() => onCssLoaded());
 }
diff --git a/ui/src/common/protos_unittest.ts b/ui/src/common/protos_unittest.ts
deleted file mode 100644
index daa9c57..0000000
--- a/ui/src/common/protos_unittest.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2018 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 protos from '../protos';
-
-test('round trip config proto', () => {
-  const input = protos.TraceConfig.create({
-    durationMs: 42,
-  });
-  const output = protos.TraceConfig.decode(
-    protos.TraceConfig.encode(input).finish(),
-  );
-  expect(output.durationMs).toBe(42);
-});
diff --git a/ui/src/components/colorizer.ts b/ui/src/components/colorizer.ts
index ed0ead6..6603cec 100644
--- a/ui/src/components/colorizer.ts
+++ b/ui/src/components/colorizer.ts
@@ -15,8 +15,8 @@
 import {hsl} from 'color-convert';
 import {hash} from '../base/hash';
 import {featureFlags} from '../core/feature_flags';
-import {Color, HSLColor, HSLuvColor} from '../public/color';
-import {ColorScheme} from '../public/color_scheme';
+import {Color, HSLColor, HSLuvColor} from '../base/color';
+import {ColorScheme} from '../base/color_scheme';
 import {RandState, pseudoRand} from '../base/rand';
 
 // 128 would provide equal weighting between dark and light text.
diff --git a/ui/src/components/query_flamegraph.ts b/ui/src/components/query_flamegraph.ts
index c9b2a0e..47f44d0 100644
--- a/ui/src/components/query_flamegraph.ts
+++ b/ui/src/components/query_flamegraph.ts
@@ -157,7 +157,6 @@
       state: this.state.state,
       onStateChange: (state) => {
         this.state.state = state;
-        this.trace.scheduleFullRedraw();
       },
     });
   }
diff --git a/ui/src/components/query_table/query_result_tab.ts b/ui/src/components/query_table/query_result_tab.ts
index dba01ee..937e9b1 100644
--- a/ui/src/components/query_table/query_result_tab.ts
+++ b/ui/src/components/query_table/query_result_tab.ts
@@ -76,9 +76,6 @@
 
     // TODO(stevegolton): Do we really need to create this view upfront?
     this.sqlViewName = await this.createViewForDebugTrack(uuidv4());
-    if (this.sqlViewName) {
-      this.trace.scheduleFullRedraw();
-    }
   }
 
   getTitle(): string {
diff --git a/ui/src/components/sql_utils/slice.ts b/ui/src/components/sql_utils/slice.ts
index edad531..ef59ea9 100644
--- a/ui/src/components/sql_utils/slice.ts
+++ b/ui/src/components/sql_utils/slice.ts
@@ -105,7 +105,7 @@
     threadDur: LONG_NULL,
     threadTs: LONG_NULL,
     category: STR_NULL,
-    argSetId: NUM,
+    argSetId: NUM_NULL,
     absTime: STR_NULL,
   });
 
@@ -135,7 +135,9 @@
       threadDur: it.threadDur ?? undefined,
       threadTs: exists(it.threadTs) ? Time.fromRaw(it.threadTs) : undefined,
       category: it.category ?? undefined,
-      args: await getArgs(engine, asArgSetId(it.argSetId)),
+      args: exists(it.argSetId)
+        ? await getArgs(engine, asArgSetId(it.argSetId))
+        : undefined,
       absTime: it.absTime ?? undefined,
     });
   }
diff --git a/ui/src/components/tracks/add_debug_track_menu.ts b/ui/src/components/tracks/add_debug_track_menu.ts
index 0870eae..c297172 100644
--- a/ui/src/components/tracks/add_debug_track_menu.ts
+++ b/ui/src/components/tracks/add_debug_track_menu.ts
@@ -93,7 +93,7 @@
     }
   }
 
-  private renderTrackTypeSelect(trace: Trace) {
+  private renderTrackTypeSelect() {
     const options = [];
     for (const type of ['slice', 'counter']) {
       options.push(
@@ -116,7 +116,6 @@
           this.trackType = (e.target as HTMLSelectElement).value as
             | 'slice'
             | 'counter';
-          trace.scheduleFullRedraw();
         },
       },
       options,
@@ -253,7 +252,7 @@
         },
       }),
       m(FormLabel, {for: 'track_type'}, 'Track type'),
-      this.renderTrackTypeSelect(vnode.attrs.trace),
+      this.renderTrackTypeSelect(),
       renderSelect('ts'),
       this.trackType === 'slice' && renderSelect('dur'),
       this.trackType === 'slice' && renderSelect('name'),
diff --git a/ui/src/components/tracks/base_slice_track.ts b/ui/src/components/tracks/base_slice_track.ts
index 323169f..6b5ffc1 100644
--- a/ui/src/components/tracks/base_slice_track.ts
+++ b/ui/src/components/tracks/base_slice_track.ts
@@ -21,7 +21,7 @@
   drawTrackHoverTooltip,
 } from '../../base/canvas_utils';
 import {cropText} from '../../base/string_utils';
-import {colorCompare} from '../../public/color';
+import {colorCompare} from '../../base/color';
 import {UNEXPECTED_PINK} from '../colorizer';
 import {TrackEventDetails} from '../../public/selection';
 import {featureFlags} from '../../core/feature_flags';
@@ -696,6 +696,9 @@
       // rowToSlice() method.
       slices.push(this.rowToSliceInternal(it));
     }
+    for (const incomplete of this.incomplete) {
+      maxDataDepth = Math.max(maxDataDepth, incomplete.depth);
+    }
     this.maxDataDepth = maxDataDepth;
     this.onUpdatedSlices(slices);
     this.slices = slices;
diff --git a/ui/src/components/tracks/sql_table_slice_track_details_tab.ts b/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
index 0cba1fb..1aebc7d 100644
--- a/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
+++ b/ui/src/components/tracks/sql_table_slice_track_details_tab.ts
@@ -220,8 +220,6 @@
       sqlValueToReadableString(this.data.args['table_name']),
       sqlValueToNumber(this.data.args['track_id']),
     );
-
-    this.trace.scheduleFullRedraw();
   }
 
   render() {
diff --git a/ui/src/components/widgets/duration_precision_menu_items.ts b/ui/src/components/widgets/duration_precision_menu_items.ts
index 7a332f8..4eb4e30 100644
--- a/ui/src/components/widgets/duration_precision_menu_items.ts
+++ b/ui/src/components/widgets/duration_precision_menu_items.ts
@@ -31,7 +31,6 @@
         active: value === attrs.trace.timeline.durationPrecision,
         onclick: () => {
           attrs.trace.timeline.durationPrecision = value;
-          attrs.trace.scheduleFullRedraw();
         },
       });
     }
diff --git a/ui/src/components/widgets/sql/legacy_table/argument_selector.ts b/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
index e8184bd..c38a4a7 100644
--- a/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
+++ b/ui/src/components/widgets/sql/legacy_table/argument_selector.ts
@@ -22,7 +22,6 @@
   LegacyTableManager,
 } from './column';
 import {TextInput} from '../../../../widgets/text_input';
-import {scheduleFullRedraw} from '../../../../widgets/raf';
 import {hasModKey, modKey} from '../../../../base/hotkeys';
 import {MenuItem} from '../../../../widgets/menu';
 import {uuidv4} from '../../../../base/uuid';
@@ -82,7 +81,6 @@
           oninput: (event: Event) => {
             const eventTarget = event.target as HTMLTextAreaElement;
             this.searchText = eventTarget.value;
-            scheduleFullRedraw();
           },
           onkeydown: (event: KeyboardEvent) => {
             if (filtered.length === 0) return;
diff --git a/ui/src/components/widgets/sql/legacy_table/state.ts b/ui/src/components/widgets/sql/legacy_table/state.ts
index 3f174eb..fa07f8d 100644
--- a/ui/src/components/widgets/sql/legacy_table/state.ts
+++ b/ui/src/components/widgets/sql/legacy_table/state.ts
@@ -454,7 +454,6 @@
       --toIndex;
     }
     this.columns.splice(toIndex, 0, column);
-    raf.scheduleFullRedraw();
   }
 
   getSelectedColumns(): LegacyTableColumn[] {
diff --git a/ui/src/components/widgets/timestamp_format_menu.ts b/ui/src/components/widgets/timestamp_format_menu.ts
index 121d76c..58a5299 100644
--- a/ui/src/components/widgets/timestamp_format_menu.ts
+++ b/ui/src/components/widgets/timestamp_format_menu.ts
@@ -31,7 +31,6 @@
         active: value === attrs.trace.timeline.timestampFormat,
         onclick: () => {
           attrs.trace.timeline.timestampFormat = value;
-          attrs.trace.scheduleFullRedraw();
         },
       });
     }
diff --git a/ui/src/components/widgets/treetable.ts b/ui/src/components/widgets/treetable.ts
index 46f78a8..6789690 100644
--- a/ui/src/components/widgets/treetable.ts
+++ b/ui/src/components/widgets/treetable.ts
@@ -14,7 +14,6 @@
 
 import m from 'mithril';
 import {classNames} from '../../base/classnames';
-import {raf} from '../../core/raf_scheduler';
 
 interface ColumnDescriptor<T> {
   name: string;
@@ -73,7 +72,6 @@
                 } else {
                   this.collapsePath(thisPath);
                 }
-                raf.scheduleFullRedraw();
               },
             }),
             getData(row),
diff --git a/ui/src/components/widgets/vega_view.ts b/ui/src/components/widgets/vega_view.ts
index 067a037..5436bcd 100644
--- a/ui/src/components/widgets/vega_view.ts
+++ b/ui/src/components/widgets/vega_view.ts
@@ -20,7 +20,6 @@
 import {SimpleResizeObserver} from '../../base/resize_observer';
 import {Engine} from '../../trace_processor/engine';
 import {QueryError} from '../../trace_processor/query_result';
-import {scheduleFullRedraw} from '../../widgets/raf';
 import {Spinner} from '../../widgets/spinner';
 
 function isVegaLite(spec: unknown): boolean {
@@ -228,7 +227,7 @@
     }
     this._status = Status.Done;
     this.pending = undefined;
-    scheduleFullRedraw('force');
+    m.redraw();
   }
 
   private handleError(pending: Promise<vega.View>, err: unknown) {
@@ -242,7 +241,7 @@
   private setError(err: unknown) {
     this._status = Status.Error;
     this._error = getErrorMessage(err);
-    scheduleFullRedraw('force');
+    m.redraw();
   }
 
   [Symbol.dispose]() {
diff --git a/ui/src/widgets/virtual_overlay_canvas.ts b/ui/src/components/widgets/virtual_overlay_canvas.ts
similarity index 87%
rename from ui/src/widgets/virtual_overlay_canvas.ts
rename to ui/src/components/widgets/virtual_overlay_canvas.ts
index 22e5384..c2d3443 100644
--- a/ui/src/widgets/virtual_overlay_canvas.ts
+++ b/ui/src/components/widgets/virtual_overlay_canvas.ts
@@ -30,11 +30,12 @@
  */
 
 import m from 'mithril';
-import {DisposableStack} from '../base/disposable_stack';
-import {findRef, toHTMLElement} from '../base/dom_utils';
-import {Rect2D, Size2D} from '../base/geom';
-import {assertExists} from '../base/logging';
-import {VirtualCanvas} from '../base/virtual_canvas';
+import {DisposableStack} from '../../base/disposable_stack';
+import {findRef, toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
+import {assertExists} from '../../base/logging';
+import {VirtualCanvas} from '../../base/virtual_canvas';
+import {Raf} from '../../public/raf';
 
 const CANVAS_CONTAINER_REF = 'canvas-container';
 const CANVAS_OVERDRAW_PX = 300;
@@ -58,13 +59,15 @@
   // Which axes should be scrollable.
   readonly scrollAxes?: 'none' | 'x' | 'y' | 'both';
 
+  // Access to the raf. If not supplied, the canvas won't be redrawn when
+  // redraws are scheduled using the raf, only when the floating canvas moves
+  // around or is resized. Thus, this might be OK for static canvas content, but
+  // for dynamic content, you really should pass a raf.
+  readonly raf?: Raf;
+
   // Called when the canvas needs to be repainted due to a layout shift or
   // or resize.
   onCanvasRedraw?(ctx: VirtualOverlayCanvasDrawContext): void;
-
-  // Called when the canvas is resized. This will immediately be followed by a
-  // call to onCanvasRedraw().
-  onCanvasResized?(size: Size2D): void;
 }
 
 // This mithril component acts as scrolling container for tall and/or wide
@@ -73,7 +76,7 @@
 export class VirtualOverlayCanvas
   implements m.ClassComponent<VirtualOverlayCanvasAttrs>
 {
-  private readonly trash = new DisposableStack();
+  readonly trash = new DisposableStack();
   private ctx?: CanvasRenderingContext2D;
   private virtualCanvas?: VirtualCanvas;
   private attrs?: VirtualOverlayCanvasAttrs;
@@ -134,7 +137,6 @@
       const dpr = window.devicePixelRatio;
       canvas.width = width * dpr;
       canvas.height = height * dpr;
-      assertExists(this.attrs).onCanvasResized?.(virtualCanvas.size);
     });
 
     // Whenever the canvas changes size or moves around (e.g. when scrolling),
@@ -143,13 +145,19 @@
     virtualCanvas.setLayoutShiftListener(() => {
       this.redrawCanvas();
     });
+
+    if (this.attrs?.raf) {
+      this.trash.use(
+        this.attrs.raf.addCanvasRedrawCallback(() => this.redrawCanvas()),
+      );
+    }
   }
 
   onremove() {
     this.trash.dispose();
   }
 
-  private redrawCanvas() {
+  redrawCanvas() {
     const ctx = assertExists(this.ctx);
     const virtualCanvas = assertExists(this.virtualCanvas);
 
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index c1045e2..e21c745 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -36,6 +36,8 @@
 import {ServiceWorkerController} from '../frontend/service_worker_controller';
 import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
 import {featureFlags} from './feature_flags';
+import {Raf} from '../public/raf';
+import {AsyncLimiter} from '../base/async_limiter';
 
 // The args that frontend/index.ts passes when calling AppImpl.initialize().
 // This is to deal with injections that would otherwise cause circular deps.
@@ -71,6 +73,7 @@
   readonly initArgs: AppInitArgs;
   readonly embeddedMode: boolean;
   readonly testingMode: boolean;
+  readonly openTraceAsyncLimiter = new AsyncLimiter();
 
   // This is normally empty and is injected with extra google-internal packages
   // via is_internal_user.js
@@ -214,8 +217,8 @@
     return this.appCtx.currentTrace?.forPlugin(this.pluginId);
   }
 
-  scheduleFullRedraw(force?: 'force'): void {
-    raf.scheduleFullRedraw(force);
+  get raf(): Raf {
+    return raf;
   }
 
   get httpRpc() {
@@ -249,30 +252,35 @@
   }
 
   private async openTrace(src: TraceSource) {
-    this.appCtx.closeCurrentTrace();
-    this.appCtx.isLoadingTrace = true;
-    try {
-      // loadTrace() in trace_loader.ts will do the following:
-      // - Create a new engine.
-      // - Pump the data from the TraceSource into the engine.
-      // - Do the initial queries to build the TraceImpl object
-      // - Call AppImpl.setActiveTrace(TraceImpl)
-      // - Continue with the trace loading logic (track decider, plugins, etc)
-      // - Resolve the promise when everything is done.
-      await loadTrace(this, src);
-      this.omnibox.reset(/* focus= */ false);
-      // loadTrace() internally will call setActiveTrace() and change our
-      // _currentTrace in the middle of its ececution. We cannot wait for
-      // loadTrace to be finished before setting it because some internal
-      // implementation details of loadTrace() rely on that trace to be current
-      // to work properly (mainly the router hash uuid).
-    } catch (err) {
-      this.omnibox.showStatusMessage(`${err}`);
-      throw err;
-    } finally {
-      this.appCtx.isLoadingTrace = false;
-      raf.scheduleFullRedraw();
-    }
+    // Rationale for asyncLimiter: openTrace takes several seconds and involves
+    // a long sequence of async tasks (e.g. invoking plugins' onLoad()). These
+    // tasks cannot overlap if the user opens traces in rapid succession, as
+    // they will mess up the state of registries. So once we start, we must
+    // complete trace loading (we don't bother supporting cancellations. If the
+    // user is too bothered, they can reload the tab).
+    this.appCtx.openTraceAsyncLimiter.schedule(async () => {
+      this.appCtx.closeCurrentTrace();
+      this.appCtx.isLoadingTrace = true;
+      try {
+        // loadTrace() in trace_loader.ts will do the following:
+        // - Create a new engine.
+        // - Pump the data from the TraceSource into the engine.
+        // - Do the initial queries to build the TraceImpl object
+        // - Call AppImpl.setActiveTrace(TraceImpl)
+        // - Continue with the trace loading logic (track decider, plugins, etc)
+        // - Resolve the promise when everything is done.
+        await loadTrace(this, src);
+        this.omnibox.reset(/* focus= */ false);
+        // loadTrace() internally will call setActiveTrace() and change our
+        // _currentTrace in the middle of its ececution. We cannot wait for
+        // loadTrace to be finished before setting it because some internal
+        // implementation details of loadTrace() rely on that trace to be current
+        // to work properly (mainly the router hash uuid).
+      } finally {
+        this.appCtx.isLoadingTrace = false;
+        raf.scheduleFullRedraw();
+      }
+    });
   }
 
   // Called by trace_loader.ts soon after it has created a new TraceImpl.
diff --git a/ui/src/core/channels.ts b/ui/src/core/channels.ts
index 22cf8e0..c8fcc99 100644
--- a/ui/src/core/channels.ts
+++ b/ui/src/core/channels.ts
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {raf} from './raf_scheduler';
-
 export const DEFAULT_CHANNEL = 'stable';
 const CHANNEL_KEY = 'perfettoUiChannel';
 
@@ -45,5 +43,4 @@
   getCurrentChannel(); // Cache the current channel before mangling next one.
   nextChannel = channel;
   localStorage.setItem(CHANNEL_KEY, channel);
-  raf.scheduleFullRedraw();
 }
diff --git a/ui/src/core/color_unittest.ts b/ui/src/core/color_unittest.ts
index acc5cb5..9bf75ea 100644
--- a/ui/src/core/color_unittest.ts
+++ b/ui/src/core/color_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor, hslToRgb, HSLuvColor} from '../public/color';
+import {HSLColor, hslToRgb, HSLuvColor} from '../base/color';
 
 describe('HSLColor', () => {
   const col = new HSLColor({h: 123, s: 66, l: 45});
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
index ad0f482..2f6f0c3 100644
--- a/ui/src/core/command_manager.ts
+++ b/ui/src/core/command_manager.ts
@@ -43,7 +43,7 @@
   runCommand(id: string, ...args: unknown[]): unknown {
     const cmd = this.registry.get(id);
     const res = cmd.callback(...args);
-    Promise.resolve(res).finally(() => raf.scheduleFullRedraw('force'));
+    Promise.resolve(res).finally(() => raf.scheduleFullRedraw());
     return res;
   }
 
diff --git a/ui/src/core/cookie_consent.ts b/ui/src/core/cookie_consent.ts
index 471f928..c1aff04 100644
--- a/ui/src/core/cookie_consent.ts
+++ b/ui/src/core/cookie_consent.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {raf} from './raf_scheduler';
 import {AppImpl} from './app_impl';
 
 const COOKIE_ACK_KEY = 'cookieAck';
@@ -59,7 +58,6 @@
             onclick: () => {
               this.showCookieConsent = false;
               localStorage.setItem(COOKIE_ACK_KEY, 'true');
-              raf.scheduleFullRedraw();
             },
           },
           'OK',
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index be3a414..8a17ad3 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -34,9 +34,7 @@
   'dev.perfetto.AndroidPerf',
   'dev.perfetto.AndroidPerfTraceCounters',
   'dev.perfetto.AndroidStartup',
-  'dev.perfetto.AsyncSlices',
   'dev.perfetto.BookmarkletApi',
-  'dev.perfetto.Counter',
   'dev.perfetto.CpuFreq',
   'dev.perfetto.CpuProfile',
   'dev.perfetto.CpuSlices',
@@ -46,7 +44,9 @@
   'dev.perfetto.FlagsPage',
   'dev.perfetto.Frames',
   'dev.perfetto.Ftrace',
+  'dev.perfetto.GpuFreq',
   'dev.perfetto.HeapProfile',
+  'dev.perfetto.InstrumentsSamplesProfile',
   'dev.perfetto.LargeScreensPerf',
   'dev.perfetto.MetricsPage',
   'dev.perfetto.PerfSamplesProfile',
@@ -57,15 +57,20 @@
   'dev.perfetto.ProcessThreadGroups',
   'dev.perfetto.QueryPage',
   'dev.perfetto.RecordTrace',
+  'dev.perfetto.RecordTraceV2',
   'dev.perfetto.RestorePinnedTrack',
   'dev.perfetto.Sched',
   'dev.perfetto.Screenshots',
+  'dev.perfetto.StandardGroups',
   'dev.perfetto.SqlModules',
+  'dev.perfetto.SysUIWorkspace',
   'dev.perfetto.Thread',
   'dev.perfetto.ThreadState',
   'dev.perfetto.TimelineSync',
   'dev.perfetto.TraceInfoPage',
   'dev.perfetto.TraceMetadata',
+  'dev.perfetto.TraceProcessorTrack',
+  'dev.perfetto.TrackEvent',
   'dev.perfetto.VizPage',
   'org.Chromium.OpenTableCommands',
   'org.kernel.LinuxKernelSubsystems',
diff --git a/ui/src/core/flow_manager.ts b/ui/src/core/flow_manager.ts
index 7529ef7..ac7b5d7 100644
--- a/ui/src/core/flow_manager.ts
+++ b/ui/src/core/flow_manager.ts
@@ -23,7 +23,6 @@
 } from '../public/track_kinds';
 import {TrackDescriptor, TrackManager} from '../public/track';
 import {AreaSelection, Selection, SelectionManager} from '../public/selection';
-import {raf} from './raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 
 const SHOW_INDIRECT_PRECEDING_FLOWS_FLAG = featureFlags.register({
@@ -448,12 +447,10 @@
         }
       }
     }
-    raf.scheduleFullRedraw();
   }
 
   private setSelectedFlows(selectedFlows: Flow[]) {
     this._selectedFlows = selectedFlows;
-    raf.scheduleFullRedraw();
   }
 
   updateFlows(selection: Selection) {
@@ -511,7 +508,6 @@
       );
       this._focusedFlowIdRight = nextFlowId;
     }
-    raf.scheduleFullRedraw();
   }
 
   // Select the slice connected to the flow in focus
@@ -563,7 +559,6 @@
 
   setCategoryVisible(name: string, value: boolean) {
     this._visibleCategories.set(name, value);
-    raf.scheduleFullRedraw();
   }
 }
 
diff --git a/ui/src/core/load_trace.ts b/ui/src/core/load_trace.ts
index ae24b1c..a888659 100644
--- a/ui/src/core/load_trace.ts
+++ b/ui/src/core/load_trace.ts
@@ -147,7 +147,7 @@
       ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
     });
   }
-  engine.onResponseReceived = () => raf.scheduleFullRedraw('force');
+  engine.onResponseReceived = () => raf.scheduleFullRedraw();
 
   if (isMetatracingEnabled()) {
     engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
@@ -287,9 +287,6 @@
 
   updateStatus(trace, 'Creating processes summaries');
   await engine.query(`include perfetto module viz.summary.processes;`);
-
-  updateStatus(trace, 'Creating track summaries');
-  await engine.query(`include perfetto module viz.summary.tracks;`);
 }
 
 function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void {
diff --git a/ui/src/core/note_manager.ts b/ui/src/core/note_manager.ts
index 20a17fe..639cc09 100644
--- a/ui/src/core/note_manager.ts
+++ b/ui/src/core/note_manager.ts
@@ -20,7 +20,6 @@
   SpanNote,
 } from '../public/note';
 import {randomColor} from '../components/colorizer';
-import {raf} from './raf_scheduler';
 
 export class NoteManagerImpl implements NoteManager {
   private _lastNodeId = 0;
@@ -55,7 +54,6 @@
       color: args.color ?? randomColor(),
       text: args.text ?? '',
     });
-    raf.scheduleFullRedraw();
     return id;
   }
 
@@ -68,7 +66,6 @@
       color: args.color ?? randomColor(),
       text: args.text ?? '',
     });
-    raf.scheduleFullRedraw();
     return id;
   }
 
@@ -81,11 +78,9 @@
       color: args.color ?? note.color,
       text: args.text ?? note.text,
     });
-    raf.scheduleFullRedraw();
   }
 
   removeNote(id: string) {
-    raf.scheduleFullRedraw();
     this._notes.delete(id);
     this.onNoteDeleted?.(id);
   }
diff --git a/ui/src/core/omnibox_manager.ts b/ui/src/core/omnibox_manager.ts
index 08f67f3..fdeed19 100644
--- a/ui/src/core/omnibox_manager.ts
+++ b/ui/src/core/omnibox_manager.ts
@@ -79,7 +79,6 @@
   focus(cursorPlacement?: number): void {
     this._focusOmniboxNextRender = true;
     this._pendingCursorPlacement = cursorPlacement;
-    raf.scheduleFullRedraw();
   }
 
   clearFocusFlag(): void {
@@ -92,7 +91,6 @@
     this._focusOmniboxNextRender = focus;
     this._omniboxSelectionIndex = 0;
     this.rejectPendingPrompt();
-    raf.scheduleFullRedraw();
   }
 
   showStatusMessage(msg: string, durationMs = 2000) {
@@ -104,7 +102,6 @@
       }, durationMs);
     }
     this._statusMessageContainer = statusMessageContainer;
-    raf.scheduleFullRedraw();
   }
 
   get statusMessage(): string | undefined {
@@ -127,7 +124,6 @@
     this._omniboxSelectionIndex = 0;
     this.rejectPendingPrompt();
     this._focusOmniboxNextRender = true;
-    raf.scheduleFullRedraw();
 
     if (choices && 'getName' in choices) {
       return new Promise<T | undefined>((resolve) => {
@@ -175,7 +171,6 @@
     this.setMode(defaultMode, focus);
     this._omniboxSelectionIndex = 0;
     this._statusMessageContainer = {};
-    raf.scheduleFullRedraw();
   }
 
   private rejectPendingPrompt() {
diff --git a/ui/src/core/perf_manager.ts b/ui/src/core/perf_manager.ts
index e63e7e8..e144179 100644
--- a/ui/src/core/perf_manager.ts
+++ b/ui/src/core/perf_manager.ts
@@ -15,6 +15,7 @@
 import m from 'mithril';
 import {raf} from './raf_scheduler';
 import {PerfStats, PerfStatsContainer, runningStatStr} from './perf_stats';
+import {MithrilEvent} from '../base/mithril_utils';
 
 export class PerfManager {
   private _enabled = false;
@@ -79,12 +80,15 @@
       m(
         'button.close-button',
         {
-          onclick: () => (attrs.perfMgr.enabled = false),
+          onclick: () => {
+            attrs.perfMgr.enabled = false;
+            raf.scheduleFullRedraw();
+          },
         },
         m('i.material-icons', 'close'),
       ),
       attrs.perfMgr.containers.map((c, i) =>
-        m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
+        m('section', m('div', `Container #${i + 1}`), c.renderPerfStats()),
       ),
     );
   }
@@ -95,7 +99,12 @@
       m('div', [
         m(
           'button',
-          {onclick: () => raf.scheduleCanvasRedraw()},
+          {
+            onclick: (e: MithrilEvent) => {
+              e.redraw = false;
+              raf.scheduleCanvasRedraw();
+            },
+          },
           'Do Canvas Redraw',
         ),
         '   |   ',
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index e4124bf..2d9bb0f 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -14,17 +14,9 @@
 
 import {PerfStats} from './perf_stats';
 import m from 'mithril';
-import {featureFlags} from './feature_flags';
+import {Raf, RedrawCallback} from '../public/raf';
 
 export type AnimationCallback = (lastFrameMs: number) => void;
-export type RedrawCallback = () => void;
-
-export const AUTOREDRAW_FLAG = featureFlags.register({
-  id: 'mithrilAutoredraw',
-  name: 'Enable Mithril autoredraw',
-  description: 'Turns calls to schedulefullRedraw() a no-op',
-  defaultValue: true,
-});
 
 // This class orchestrates all RAFs in the UI. It ensures that there is only
 // one animation frame handler overall and that callbacks are called in
@@ -34,7 +26,7 @@
 // - redraw callbacks that will repaint canvases.
 // This class guarantees that, on each frame, redraw callbacks are called after
 // all action callbacks.
-export class RafScheduler {
+export class RafScheduler implements Raf {
   // These happen at the beginning of any animation frame. Used by Animation.
   private animationCallbacks = new Set<AnimationCallback>();
 
@@ -61,7 +53,7 @@
   constructor() {
     // Patch m.redraw() to our RAF full redraw.
     const origSync = m.redraw.sync;
-    const redrawFn = () => this.scheduleFullRedraw('force');
+    const redrawFn = () => this.scheduleFullRedraw();
     redrawFn.sync = origSync;
     m.redraw = redrawFn;
 
@@ -71,10 +63,7 @@
   // Schedule re-rendering of virtual DOM and canvas.
   // If a callback is passed it will be executed after the DOM redraw has
   // completed.
-  scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
-    // If we are using autoredraw mode, make this function a no-op unless
-    // 'force' is passed.
-    if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
+  scheduleFullRedraw(cb?: RedrawCallback) {
     this.requestedFullRedraw = true;
     cb && this.postRedrawCallbacks.push(cb);
     this.maybeScheduleAnimationFrame(true);
@@ -120,7 +109,6 @@
 
   setPerfStatsEnabled(enabled: boolean) {
     this.recordPerfStats = enabled;
-    this.scheduleFullRedraw();
   }
 
   get hasPendingRedraws(): boolean {
@@ -153,10 +141,8 @@
       redraw?: () => void,
     ) => void;
 
-    mithrilRender(
-      element,
-      component !== null ? m(component) : null,
-      AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
+    mithrilRender(element, component !== null ? m(component) : null, () =>
+      this.scheduleFullRedraw(),
     );
   }
 
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
index c732b91..00524d8 100644
--- a/ui/src/core/scroll_helper.ts
+++ b/ui/src/core/scroll_helper.ts
@@ -114,7 +114,7 @@
   private verticalScrollToTrack(trackUri: string, openGroup: boolean) {
     // Find the actual track node that uses that URI, we need various properties
     // from it.
-    const trackNode = this.workspace.findTrackByUri(trackUri);
+    const trackNode = this.workspace.getTrackByUri(trackUri);
     if (!trackNode) return;
 
     // Try finding the track directly.
diff --git a/ui/src/core/search_manager.ts b/ui/src/core/search_manager.ts
index 3f2c6a9..f084b00 100644
--- a/ui/src/core/search_manager.ts
+++ b/ui/src/core/search_manager.ts
@@ -90,7 +90,6 @@
         raf.scheduleFullRedraw();
       });
     }
-    raf.scheduleFullRedraw();
   }
 
   reset() {
@@ -158,7 +157,6 @@
         source: this._results.sources[this._resultIndex],
       });
     }
-    raf.scheduleFullRedraw();
   }
 
   private setResultIndexWithSaturation(nextIndex: number) {
diff --git a/ui/src/core/selection_aggregation_manager.ts b/ui/src/core/selection_aggregation_manager.ts
index cba366f..f293e12 100644
--- a/ui/src/core/selection_aggregation_manager.ts
+++ b/ui/src/core/selection_aggregation_manager.ts
@@ -16,6 +16,8 @@
 import {isString} from '../base/object_utils';
 import {AggregateData, Column, ColumnDef, Sorting} from '../public/aggregation';
 import {AreaSelection, AreaSelectionAggregator} from '../public/selection';
+import {TrackDescriptor} from '../public/track';
+import {Dataset, UnionDataset} from '../trace_processor/dataset';
 import {Engine} from '../trace_processor/engine';
 import {NUM} from '../trace_processor/query_result';
 import {raf} from './raf_scheduler';
@@ -104,7 +106,13 @@
     aggr: AreaSelectionAggregator,
     area: AreaSelection,
   ): Promise<AggregateData | undefined> {
-    const viewExists = await aggr.createAggregateView(this.engine, area);
+    const dataset = this.createDatasetForAggregator(aggr, area.tracks);
+    const viewExists = await aggr.createAggregateView(
+      this.engine,
+      area,
+      dataset,
+    );
+
     if (!viewExists) {
       return undefined;
     }
@@ -173,6 +181,26 @@
     return data;
   }
 
+  private createDatasetForAggregator(
+    aggr: AreaSelectionAggregator,
+    tracks: ReadonlyArray<TrackDescriptor>,
+  ): Dataset | undefined {
+    const filteredDatasets = tracks
+      .filter(
+        (td) =>
+          aggr.trackKind === undefined || aggr.trackKind === td.tags?.kind,
+      )
+      .map((td) => td.track.getDataset?.())
+      .filter((dataset) => dataset !== undefined)
+      .filter(
+        (dataset) =>
+          aggr.schema === undefined || dataset.implements(aggr.schema),
+      );
+
+    if (filteredDatasets.length === 0) return undefined;
+    return new UnionDataset(filteredDatasets).optimize();
+  }
+
   private async getSum(tableName: string, def: ColumnDef): Promise<string> {
     if (!def.sum) return '';
     const result = await this.engine.query(
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 44303d2..a688e08 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -237,7 +237,6 @@
   private setSelection(selection: Selection, opts?: SelectionOpts) {
     this._selection = selection;
     this.onSelectionChange(selection, opts ?? {});
-    raf.scheduleFullRedraw();
 
     if (opts?.scrollToSelection) {
       this.scrollToCurrentSelection();
@@ -403,7 +402,11 @@
         }
       }
     } else if (sel.kind === 'track_event') {
-      return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+      // Pretend incomplete slices are instants. The -1 duration here is just a
+      // flag, and doesn't actually represent the duration of the event.
+      // Besides, TimeSpan's will throw if created with a negative duration.
+      const dur = sel.dur === -1n ? 0n : sel.dur;
+      return TimeSpan.fromTimeAndDuration(sel.ts, dur);
     }
 
     return undefined;
diff --git a/ui/src/core/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
index 9de9b90..e8000ae 100644
--- a/ui/src/core/sidebar_manager.ts
+++ b/ui/src/core/sidebar_manager.ts
@@ -14,7 +14,6 @@
 
 import {Registry} from '../base/registry';
 import {SidebarManager, SidebarMenuItem} from '../public/sidebar';
-import {raf} from './raf_scheduler';
 
 export type SidebarMenuItemInternal = SidebarMenuItem & {
   id: string; // A unique id generated by this class at registration time.
@@ -47,6 +46,5 @@
   public toggleVisibility() {
     if (!this.enabled) return;
     this._visible = !this._visible;
-    raf.scheduleFullRedraw();
   }
 }
diff --git a/ui/src/core/state_serialization.ts b/ui/src/core/state_serialization.ts
index 0ad4872..ddbaecc 100644
--- a/ui/src/core/state_serialization.ts
+++ b/ui/src/core/state_serialization.ts
@@ -179,7 +179,7 @@
 
   // Restore the pinned tracks, if they exist.
   for (const uri of appState.pinnedTracks) {
-    const track = trace.workspace.findTrackByUri(uri);
+    const track = trace.workspace.getTrackByUri(uri);
     if (track) {
       track.pin();
     }
diff --git a/ui/src/core/tab_manager.ts b/ui/src/core/tab_manager.ts
index d889f96..3273122 100644
--- a/ui/src/core/tab_manager.ts
+++ b/ui/src/core/tab_manager.ts
@@ -18,7 +18,6 @@
   SplitPanelDrawerVisibility,
   toggleVisibility,
 } from '../widgets/split_panel';
-import {raf} from './raf_scheduler';
 
 export interface ResolvedTab {
   uri: string;
@@ -98,8 +97,6 @@
     ) {
       this.setTabPanelVisibility(SplitPanelDrawerVisibility.VISIBLE);
     }
-
-    raf.scheduleFullRedraw();
   }
 
   // Hide a tab in the tab bar pick a new tab to show.
@@ -131,7 +128,6 @@
       // Otherwise just remove the tab
       this._openTabs = this._openTabs.filter((x) => x !== uri);
     }
-    raf.scheduleFullRedraw();
   }
 
   toggleTab(uri: string): void {
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index 0efc660..80d32a9 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue, assertUnreachable} from '../base/logging';
+import {assertUnreachable} from '../base/logging';
 import {Time, time, TimeSpan} from '../base/time';
 import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
-import {Area} from '../public/selection';
 import {raf} from './raf_scheduler';
 import {HighPrecisionTime} from '../base/high_precision_time';
 import {DurationPrecision, Timeline, TimestampFormat} from '../public/timeline';
@@ -45,6 +44,14 @@
   private _hoveredUtid?: number;
   private _hoveredPid?: number;
 
+  // This is used to mark the timeline of the area that is currently being
+  // selected.
+  //
+  // TODO(stevegolton): This shouldn't really be in the global timeline state,
+  // it's really only a concept of the viewer page and should be moved there
+  // instead.
+  selectedSpan?: {start: time; end: time};
+
   get highlightedSliceId() {
     return this._highlightedSliceId;
   }
@@ -81,9 +88,6 @@
     raf.scheduleCanvasRedraw();
   }
 
-  // This is used to calculate the tracks within a Y range for area selection.
-  private _selectedArea?: Area;
-
   constructor(private readonly traceInfo: TraceInfo) {
     this._visibleWindow = HighPrecisionTimeSpan.fromTime(
       traceInfo.start,
@@ -125,29 +129,6 @@
     this.updateVisibleTimeHP(newWindow);
   }
 
-  // Set the highlight box to draw
-  selectArea(
-    start: time,
-    end: time,
-    tracks = this._selectedArea ? this._selectedArea.trackUris : [],
-  ) {
-    assertTrue(
-      end >= start,
-      `Impossible select area: start [${start}] >= end [${end}]`,
-    );
-    this._selectedArea = {start, end, trackUris: tracks};
-    raf.scheduleFullRedraw();
-  }
-
-  deselectArea() {
-    this._selectedArea = undefined;
-    raf.scheduleCanvasRedraw();
-  }
-
-  get selectedArea(): Area | undefined {
-    return this._selectedArea;
-  }
-
   // Set visible window using an integer time span
   updateVisibleTime(ts: TimeSpan) {
     this.updateVisibleTimeHP(HighPrecisionTimeSpan.fromTime(ts.start, ts.end));
@@ -159,6 +140,24 @@
     this.updateVisibleTime(new TimeSpan(start, end));
   }
 
+  moveStart(delta: number) {
+    this.updateVisibleTimeHP(
+      new HighPrecisionTimeSpan(
+        this._visibleWindow.start.addNumber(delta),
+        this.visibleWindow.duration - delta,
+      ),
+    );
+  }
+
+  moveEnd(delta: number) {
+    this.updateVisibleTimeHP(
+      new HighPrecisionTimeSpan(
+        this._visibleWindow.start,
+        this.visibleWindow.duration + delta,
+      ),
+    );
+  }
+
   // Set visible window using a high precision time span
   updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
     this._visibleWindow = ts
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index d86a310..88d05f6 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -52,6 +52,7 @@
 import {PostedTrace} from './trace_source';
 import {PerfManager} from './perf_manager';
 import {EvtSource} from '../base/events';
+import {Raf} from '../public/raf';
 
 /**
  * Handles the per-trace state of the UI
@@ -419,8 +420,8 @@
     };
   }
 
-  scheduleFullRedraw(): void {
-    this.appImpl.scheduleFullRedraw();
+  get raf(): Raf {
+    return this.appImpl.raf;
   }
 
   navigate(newHash: string): void {
diff --git a/ui/src/core/workspace_manager.ts b/ui/src/core/workspace_manager.ts
index 77803b7..6990421 100644
--- a/ui/src/core/workspace_manager.ts
+++ b/ui/src/core/workspace_manager.ts
@@ -14,36 +14,44 @@
 
 import {assertTrue} from '../base/logging';
 import {Workspace, WorkspaceManager} from '../public/workspace';
-import {raf} from './raf_scheduler';
 
 const DEFAULT_WORKSPACE_NAME = 'Default Workspace';
 
 export class WorkspaceManagerImpl implements WorkspaceManager {
+  readonly defaultWorkspace = new Workspace();
   private _workspaces: Workspace[] = [];
   private _currentWorkspace: Workspace;
 
   constructor() {
-    // TS compiler cannot see that we are indirectly initializing
-    // _currentWorkspace via resetWorkspaces(), hence the re-assignment.
-    this._currentWorkspace = this.createEmptyWorkspace(DEFAULT_WORKSPACE_NAME);
+    this.defaultWorkspace.title = DEFAULT_WORKSPACE_NAME;
+    this._currentWorkspace = this.defaultWorkspace;
   }
 
   createEmptyWorkspace(title: string): Workspace {
     const workspace = new Workspace();
     workspace.title = title;
-    workspace.onchange = () => raf.scheduleFullRedraw();
     this._workspaces.push(workspace);
     return workspace;
   }
 
+  removeWorkspace(ws: Workspace) {
+    if (ws === this.currentWorkspace) {
+      this._currentWorkspace = this.defaultWorkspace;
+    }
+    this._workspaces = this._workspaces.filter((w) => w !== ws);
+  }
+
   switchWorkspace(workspace: Workspace): void {
     // If this fails the workspace doesn't come from createEmptyWorkspace().
-    assertTrue(this._workspaces.includes(workspace));
+    assertTrue(
+      this._workspaces.includes(workspace) ||
+        workspace === this.defaultWorkspace,
+    );
     this._currentWorkspace = workspace;
   }
 
   get all(): ReadonlyArray<Workspace> {
-    return this._workspaces;
+    return [this.defaultWorkspace].concat(this._workspaces);
   }
 
   get currentWorkspace() {
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index adc3bce..e6087fe 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -99,6 +99,17 @@
   defaultValue: false,
 });
 
+function getOrPromptForTimestamp(tsRaw: unknown): time | undefined {
+  if (exists(tsRaw)) {
+    if (typeof tsRaw !== 'bigint') {
+      throw Error(`${tsRaw} is not a bigint`);
+    }
+    return Time.fromRaw(tsRaw);
+  }
+  // No args passed, probably run from the command palette.
+  return promptForTimestamp('Enter a timestamp');
+}
+
 export default class implements PerfettoPlugin {
   static readonly id = 'perfetto.CoreCommands';
   static onActivate(ctx: App) {
@@ -250,17 +261,22 @@
       id: 'perfetto.CoreCommands#PanToTimestamp',
       name: 'Pan to timestamp',
       callback: (tsRaw: unknown) => {
-        if (exists(tsRaw)) {
-          if (typeof tsRaw !== 'bigint') {
-            throw Error(`${tsRaw} is not a bigint`);
-          }
-          ctx.timeline.panToTimestamp(Time.fromRaw(tsRaw));
-        } else {
-          // No args passed, probably run from the command palette.
-          const ts = promptForTimestamp('Enter a timestamp');
-          if (exists(ts)) {
-            ctx.timeline.panToTimestamp(Time.fromRaw(ts));
-          }
+        const ts = getOrPromptForTimestamp(tsRaw);
+        if (ts !== undefined) {
+          ctx.timeline.panToTimestamp(ts);
+        }
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.CoreCommands#MarkTimestamp',
+      name: 'Mark timestamp',
+      callback: (tsRaw: unknown) => {
+        const ts = getOrPromptForTimestamp(tsRaw);
+        if (ts !== undefined) {
+          ctx.notes.addNote({
+            timestamp: ts,
+          });
         }
       },
     });
@@ -277,7 +293,7 @@
       id: 'createNewEmptyWorkspace',
       name: 'Create new empty workspace',
       callback: async () => {
-        const workspaces = AppImpl.instance.trace?.workspaces;
+        const workspaces = ctx.workspaces;
         if (workspaces === undefined) return; // No trace loaded.
         const name = await ctx.omnibox.prompt('Give it a name...');
         if (name === undefined || name === '') return;
@@ -289,7 +305,7 @@
       id: 'switchWorkspace',
       name: 'Switch workspace',
       callback: async () => {
-        const workspaces = AppImpl.instance.trace?.workspaces;
+        const workspaces = ctx.workspaces;
         if (workspaces === undefined) return; // No trace loaded.
         const workspace = await ctx.omnibox.prompt('Choose a workspace...', {
           values: workspaces.all,
diff --git a/ui/src/core_plugins/flags_page/flags_page.ts b/ui/src/core_plugins/flags_page/flags_page.ts
index d8cfdf6..fc21d3f 100644
--- a/ui/src/core_plugins/flags_page/flags_page.ts
+++ b/ui/src/core_plugins/flags_page/flags_page.ts
@@ -16,7 +16,6 @@
 import {channelChanged, getNextChannel, setChannel} from '../../core/channels';
 import {featureFlags} from '../../core/feature_flags';
 import {Flag, OverrideState} from '../../public/feature_flag';
-import {raf} from '../../core/raf_scheduler';
 import {PageAttrs} from '../../public/page';
 import {Router} from '../../core/router';
 
@@ -52,7 +51,6 @@
           onchange: (e: InputEvent) => {
             const value = (e.target as HTMLSelectElement).value;
             attrs.onSelect(value);
-            raf.scheduleFullRedraw();
           },
         },
         attrs.options.map((o) => {
@@ -139,7 +137,6 @@
           {
             onclick: () => {
               featureFlags.resetAll();
-              raf.scheduleFullRedraw();
             },
           },
           'Reset all below',
diff --git a/ui/src/core_plugins/flags_page/plugins_page.ts b/ui/src/core_plugins/flags_page/plugins_page.ts
index ed5bd0b..4d8c7bf 100644
--- a/ui/src/core_plugins/flags_page/plugins_page.ts
+++ b/ui/src/core_plugins/flags_page/plugins_page.ts
@@ -20,7 +20,6 @@
 import {PageAttrs} from '../../public/page';
 import {AppImpl} from '../../core/app_impl';
 import {PluginWrapper} from '../../core/plugin_manager';
-import {raf} from '../../core/raf_scheduler';
 
 // This flag indicated whether we need to restart the UI to apply plugin
 // changes. It is purposely a global as we want it to outlive the Mithril
@@ -50,7 +49,6 @@
               plugin.enableFlag.set(false);
             }
             needsRestart = true;
-            raf.scheduleFullRedraw();
           },
         }),
         m(Button, {
@@ -61,7 +59,6 @@
               plugin.enableFlag.set(true);
             }
             needsRestart = true;
-            raf.scheduleFullRedraw();
           },
         }),
         m(Button, {
@@ -72,7 +69,6 @@
               plugin.enableFlag.reset();
             }
             needsRestart = true;
-            raf.scheduleFullRedraw();
           },
         }),
       ),
@@ -114,7 +110,6 @@
             plugin.enableFlag.set(true);
           }
           needsRestart = true;
-          raf.scheduleFullRedraw();
         },
       }),
       exists(loadTime)
diff --git a/ui/src/core_plugins/global_groups/index.ts b/ui/src/core_plugins/global_groups/index.ts
index 4ae9e27..fbc4388 100644
--- a/ui/src/core_plugins/global_groups/index.ts
+++ b/ui/src/core_plugins/global_groups/index.ts
@@ -14,66 +14,12 @@
 
 import {PerfettoPlugin} from '../../public/plugin';
 import {Trace} from '../../public/trace';
-import {TrackNode} from '../../public/workspace';
-
-const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
-const MEM_DMA = 'mem.dma_buffer';
-const MEM_ION = 'mem.ion';
-const F2FS_IOSTAT_TAG = 'f2fs_iostat.';
-const F2FS_IOSTAT_GROUP_NAME = 'f2fs_iostat';
-const F2FS_IOSTAT_LAT_TAG = 'f2fs_iostat_latency.';
-const F2FS_IOSTAT_LAT_GROUP_NAME = 'f2fs_iostat_latency';
-const DISK_IOSTAT_TAG = 'diskstat.';
-const DISK_IOSTAT_GROUP_NAME = 'diskstat';
-const BUDDY_INFO_TAG = 'mem.buddyinfo';
-const UFS_CMD_TAG_REGEX = new RegExp('^io.ufs.command.tag.*$');
-const UFS_CMD_TAG_GROUP = 'io.ufs.command.tags';
-// NB: Userspace wakelocks start with "WakeLock" not "Wakelock".
-const KERNEL_WAKELOCK_REGEX = new RegExp('^Wakelock.*$');
-const KERNEL_WAKELOCK_GROUP = 'Kernel wakelocks';
-const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
-const NETWORK_TRACK_GROUP = 'Networking';
-const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
-const ENTITY_RESIDENCY_GROUP = 'Entity residency';
-const UCLAMP_REGEX = new RegExp('^UCLAMP_');
-const UCLAMP_GROUP = 'Scheduler Utilization Clamping';
-const POWER_RAILS_GROUP = 'Power Rails';
-const POWER_RAILS_REGEX = new RegExp('^power.');
-const FREQUENCY_GROUP = 'Frequency Scaling';
-const TEMPERATURE_REGEX = new RegExp('^.* Temperature$');
-const TEMPERATURE_GROUP = 'Temperature';
-const IRQ_GROUP = 'IRQs';
-const IRQ_REGEX = new RegExp('^(Irq|SoftIrq) Cpu.*');
-const CHROME_TRACK_REGEX = new RegExp('^Chrome.*|^InputLatency::.*');
-const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
-const MISC_GROUP = 'Misc Global Tracks';
 
 // This plugin is responsible for organizing all the global tracks.
 export default class implements PerfettoPlugin {
   static readonly id = 'perfetto.GlobalGroups';
   async onTraceLoad(trace: Trace): Promise<void> {
     trace.onTraceReady.addListener(() => {
-      groupGlobalIonTracks(trace);
-      groupGlobalIostatTracks(trace, F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
-      groupGlobalIostatTracks(
-        trace,
-        F2FS_IOSTAT_LAT_TAG,
-        F2FS_IOSTAT_LAT_GROUP_NAME,
-      );
-      groupGlobalIostatTracks(trace, DISK_IOSTAT_TAG, DISK_IOSTAT_GROUP_NAME);
-      groupTracksByRegex(trace, UFS_CMD_TAG_REGEX, UFS_CMD_TAG_GROUP);
-      groupGlobalBuddyInfoTracks(trace);
-      groupTracksByRegex(trace, KERNEL_WAKELOCK_REGEX, KERNEL_WAKELOCK_GROUP);
-      groupTracksByRegex(trace, NETWORK_TRACK_REGEX, NETWORK_TRACK_GROUP);
-      groupTracksByRegex(trace, ENTITY_RESIDENCY_REGEX, ENTITY_RESIDENCY_GROUP);
-      groupTracksByRegex(trace, UCLAMP_REGEX, UCLAMP_GROUP);
-      groupFrequencyTracks(trace, FREQUENCY_GROUP);
-      groupTracksByRegex(trace, POWER_RAILS_REGEX, POWER_RAILS_GROUP);
-      groupTracksByRegex(trace, TEMPERATURE_REGEX, TEMPERATURE_GROUP);
-      groupTracksByRegex(trace, IRQ_REGEX, IRQ_GROUP);
-      groupTracksByRegex(trace, CHROME_TRACK_REGEX, CHROME_TRACK_GROUP);
-      groupMiscNonAllowlistedTracks(trace, MISC_GROUP);
-
       // Move groups underneath tracks
       Array.from(trace.workspace.children)
         .sort((a, b) => {
@@ -92,171 +38,3 @@
     });
   }
 }
-
-function groupGlobalIonTracks(trace: Trace): void {
-  const ionTracks: TrackNode[] = [];
-  let hasSummary = false;
-
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-
-    const isIon = track.title.startsWith(MEM_ION);
-    const isIonCounter = track.title === MEM_ION;
-    const isDmaHeapCounter = track.title === MEM_DMA_COUNTER_NAME;
-    const isDmaBuffferSlices = track.title === MEM_DMA;
-    if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
-      ionTracks.push(track);
-    }
-    hasSummary = hasSummary || isIonCounter;
-    hasSummary = hasSummary || isDmaHeapCounter;
-  }
-
-  if (ionTracks.length === 0 || !hasSummary) {
-    return;
-  }
-
-  const tracksToAddToGroup: TrackNode[] = [];
-  let memGroupNode: TrackNode | undefined;
-  for (const track of ionTracks) {
-    if (
-      !memGroupNode &&
-      [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.title)
-    ) {
-      // Create a new group that copies the details from this track
-      memGroupNode = new TrackNode({
-        uri: track.uri,
-        title: track.title,
-        isSummary: true,
-      });
-      // Remove it from the workspace as we're going to add the group later
-      track.remove();
-    } else {
-      tracksToAddToGroup.push(track);
-    }
-  }
-
-  if (memGroupNode) {
-    tracksToAddToGroup.forEach((t) => memGroupNode.addChildInOrder(t));
-    trace.workspace.addChildInOrder(memGroupNode);
-  }
-}
-
-function groupGlobalIostatTracks(
-  trace: Trace,
-  tag: string,
-  groupName: string,
-): void {
-  const devMap = new Map<string, TrackNode>();
-
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-    if (track.title.startsWith(tag)) {
-      const name = track.title.split('.', 3);
-      const key = name[1];
-
-      let parentGroup = devMap.get(key);
-      if (!parentGroup) {
-        const group = new TrackNode({title: groupName, isSummary: true});
-        trace.workspace.addChildInOrder(group);
-        devMap.set(key, group);
-        parentGroup = group;
-      }
-
-      track.title = name[2];
-      parentGroup.addChildInOrder(track);
-    }
-  }
-}
-
-function groupGlobalBuddyInfoTracks(trace: Trace): void {
-  const devMap = new Map<string, TrackNode>();
-
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-    if (track.title.startsWith(BUDDY_INFO_TAG)) {
-      const tokens = track.title.split('[');
-      const node = tokens[1].slice(0, -1);
-      const zone = tokens[2].slice(0, -1);
-      const size = tokens[3].slice(0, -1);
-
-      const groupName = 'Buddyinfo:  Node: ' + node + ' Zone: ' + zone;
-      if (!devMap.has(groupName)) {
-        const group = new TrackNode({title: groupName, isSummary: true});
-        devMap.set(groupName, group);
-        trace.workspace.addChildInOrder(group);
-      }
-      track.title = 'Chunk size: ' + size;
-      const group = devMap.get(groupName)!;
-      group.addChildInOrder(track);
-    }
-  }
-}
-
-function groupFrequencyTracks(trace: Trace, groupName: string): void {
-  const group = new TrackNode({title: groupName, isSummary: true});
-
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-    // Group all the frequency tracks together (except the CPU and GPU
-    // frequency ones).
-    if (
-      track.title.endsWith('Frequency') &&
-      !track.title.startsWith('Cpu') &&
-      !track.title.startsWith('Gpu')
-    ) {
-      group.addChildInOrder(track);
-    }
-  }
-
-  if (group.children.length > 0) {
-    trace.workspace.addChildInOrder(group);
-  }
-}
-
-function groupMiscNonAllowlistedTracks(trace: Trace, groupName: string): void {
-  // List of allowlisted track names.
-  const ALLOWLIST_REGEXES = [
-    new RegExp('^Cpu .*$', 'i'),
-    new RegExp('^Gpu .*$', 'i'),
-    new RegExp('^Trace Triggers$'),
-    new RegExp('^Android App Startups$'),
-    new RegExp('^Device State.*$'),
-    new RegExp('^Android logs$'),
-  ];
-
-  const group = new TrackNode({title: groupName, isSummary: true});
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-    let allowlisted = false;
-    for (const regex of ALLOWLIST_REGEXES) {
-      allowlisted = allowlisted || regex.test(track.title);
-    }
-    if (allowlisted) {
-      continue;
-    }
-    group.addChildInOrder(track);
-  }
-
-  if (group.children.length > 0) {
-    trace.workspace.addChildInOrder(group);
-  }
-}
-
-function groupTracksByRegex(
-  trace: Trace,
-  regex: RegExp,
-  groupName: string,
-): void {
-  const group = new TrackNode({title: groupName, isSummary: true});
-
-  for (const track of trace.workspace.children) {
-    if (track.hasChildren) continue;
-    if (regex.test(track.title)) {
-      group.addChildInOrder(track);
-    }
-  }
-
-  if (group.children.length > 0) {
-    trace.workspace.addChildInOrder(group);
-  }
-}
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index bad2bfb..708096a 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -17,7 +17,6 @@
 import {isEmptyData} from '../public/aggregation';
 import {DetailsShell} from '../widgets/details_shell';
 import {Button, ButtonBar} from '../widgets/button';
-import {raf} from '../core/raf_scheduler';
 import {EmptyState} from '../widgets/empty_state';
 import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
 import {PivotTable} from './pivot_table';
@@ -26,6 +25,7 @@
 import {
   CPU_PROFILE_TRACK_KIND,
   PERF_SAMPLES_PROFILE_TRACK_KIND,
+  INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
   SLICE_TRACK_KIND,
 } from '../public/track_kinds';
 import {
@@ -39,9 +39,13 @@
 import {Flamegraph} from '../widgets/flamegraph';
 
 interface View {
-  key: string;
-  name: string;
-  content: m.Children;
+  readonly key: string;
+  readonly name: string;
+  readonly specificity?: {
+    readonly kind: number;
+    readonly schema: number;
+  };
+  readonly content: m.Children;
 }
 
 export type AreaDetailsPanelAttrs = {trace: TraceImpl};
@@ -52,6 +56,7 @@
   private currentTab: string | undefined = undefined;
   private cpuProfileFlamegraph?: QueryFlamegraph;
   private perfSampleFlamegraph?: QueryFlamegraph;
+  private instrumentsSampleFlamegraph?: QueryFlamegraph;
   private sliceFlamegraph?: QueryFlamegraph;
 
   constructor({attrs}: m.CVnode<AreaDetailsPanelAttrs>) {
@@ -88,6 +93,12 @@
         views.push({
           key: value.tabName,
           name: value.tabName,
+          specificity: {
+            kind: aggregator.trackKind ? 1 : 0,
+            schema: aggregator.schema
+              ? Object.keys(aggregator.schema).length
+              : 0,
+          },
           content: m(AggregationPanel, {
             aggregatorId,
             data: value,
@@ -97,6 +108,23 @@
       }
     }
 
+    views.sort((a, b) => {
+      if (a.specificity === undefined || b.specificity === undefined) {
+        return 0;
+      }
+
+      if (a.specificity.kind !== b.specificity.kind) {
+        return b.specificity.kind - a.specificity.kind;
+      }
+
+      if (a.specificity.schema !== b.specificity.schema) {
+        return b.specificity.schema - a.specificity.schema;
+      }
+
+      // If all else is equal, fall back to the registration order.
+      return 0;
+    });
+
     const pivotTableState = this.trace.pivotTable.state;
     const tree = pivotTableState.queryResult?.tree;
     if (
@@ -135,7 +163,6 @@
       return m(Button, {
         onclick: () => {
           this.currentTab = key;
-          raf.scheduleFullRedraw();
         },
         key,
         label: name,
@@ -196,6 +223,17 @@
         content: this.perfSampleFlamegraph.render(),
       });
     }
+    this.instrumentsSampleFlamegraph = this.computeInstrumentsSampleFlamegraph(
+      trace,
+      isChanged,
+    );
+    if (this.instrumentsSampleFlamegraph !== undefined) {
+      views.push({
+        key: 'instruments_sample_flamegraph_selection',
+        name: 'Instruments Sample Flamegraph',
+        content: this.instrumentsSampleFlamegraph.render(),
+      });
+    }
     this.sliceFlamegraph = this.computeSliceFlamegraph(trace, isChanged);
     if (this.sliceFlamegraph !== undefined) {
       views.push({
@@ -318,6 +356,52 @@
     });
   }
 
+  private computeInstrumentsSampleFlamegraph(trace: Trace, isChanged: boolean) {
+    const currentSelection = trace.selection.selection;
+    if (currentSelection.kind !== 'area') {
+      return undefined;
+    }
+    if (!isChanged) {
+      // If the selection has not changed, just return a copy of the last seen
+      // attrs.
+      return this.instrumentsSampleFlamegraph;
+    }
+    const upids = getUpidsFromInstrumentsSampleAreaSelection(currentSelection);
+    const utids = getUtidsFromInstrumentsSampleAreaSelection(currentSelection);
+    if (utids.length === 0 && upids.length === 0) {
+      return undefined;
+    }
+    const metrics = metricsFromTableOrSubquery(
+      `
+        (
+          select id, parent_id as parentId, name, self_count
+          from _callstacks_for_callsites!((
+            select p.callsite_id
+            from instruments_sample p
+            join thread t using (utid)
+            where p.ts >= ${currentSelection.start}
+              and p.ts <= ${currentSelection.end}
+              and (
+                p.utid in (${utids.join(',')})
+                or t.upid in (${upids.join(',')})
+              )
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Instruments Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module appleos.instruments.samples',
+    );
+    return new QueryFlamegraph(trace, metrics, {
+      state: Flamegraph.createDefaultState(metrics),
+    });
+  }
+
   private computeSliceFlamegraph(trace: Trace, isChanged: boolean) {
     const currentSelection = trace.selection.selection;
     if (currentSelection.kind !== 'area') {
@@ -423,3 +507,33 @@
   }
   return utids;
 }
+
+function getUpidsFromInstrumentsSampleAreaSelection(
+  currentSelection: AreaSelection,
+) {
+  const upids = [];
+  for (const trackInfo of currentSelection.tracks) {
+    if (
+      trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
+      trackInfo.tags?.utid === undefined
+    ) {
+      upids.push(assertExists(trackInfo.tags?.upid));
+    }
+  }
+  return upids;
+}
+
+function getUtidsFromInstrumentsSampleAreaSelection(
+  currentSelection: AreaSelection,
+) {
+  const utids = [];
+  for (const trackInfo of currentSelection.tracks) {
+    if (
+      trackInfo?.tags?.kind === INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND &&
+      trackInfo.tags?.utid !== undefined
+    ) {
+      utids.push(trackInfo.tags?.utid);
+    }
+  }
+  return utids;
+}
diff --git a/ui/src/frontend/drag/border_drag_strategy.ts b/ui/src/frontend/drag/border_drag_strategy.ts
deleted file mode 100644
index c98b02d..0000000
--- a/ui/src/frontend/drag/border_drag_strategy.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2021 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 {TimeScale} from '../../base/time_scale';
-import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
-
-export class BorderDragStrategy extends DragStrategy {
-  private moveStart = false;
-
-  constructor(
-    map: TimeScale,
-    private pixelBounds: [number, number],
-    updateVizTime: DragStrategyUpdateTimeFn,
-  ) {
-    super(map, updateVizTime);
-  }
-
-  onDrag(x: number) {
-    const moveStartPx = this.moveStart ? x : this.pixelBounds[0];
-    const moveEndPx = !this.moveStart ? x : this.pixelBounds[1];
-    const tStart = this.map.pxToHpTime(Math.min(moveStartPx, moveEndPx));
-    const tEnd = this.map.pxToHpTime(Math.max(moveStartPx, moveEndPx));
-    if (moveStartPx > moveEndPx) {
-      this.moveStart = !this.moveStart;
-    }
-    super.updateGlobals(tStart, tEnd);
-    this.pixelBounds = [this.map.hpTimeToPx(tStart), this.map.hpTimeToPx(tEnd)];
-  }
-
-  onDragStart(x: number) {
-    this.moveStart =
-      Math.abs(x - this.pixelBounds[0]) < Math.abs(x - this.pixelBounds[1]);
-  }
-}
diff --git a/ui/src/frontend/drag/drag_strategy.ts b/ui/src/frontend/drag/drag_strategy.ts
deleted file mode 100644
index 0a0f3d4..0000000
--- a/ui/src/frontend/drag/drag_strategy.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2021 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 {HighPrecisionTime} from '../../base/high_precision_time';
-import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
-import {TimeScale} from '../../base/time_scale';
-
-export type DragStrategyUpdateTimeFn = (ts: HighPrecisionTimeSpan) => void;
-
-export abstract class DragStrategy {
-  constructor(
-    protected map: TimeScale,
-    private updateVizTime: DragStrategyUpdateTimeFn,
-  ) {}
-
-  abstract onDrag(x: number): void;
-
-  abstract onDragStart(x: number): void;
-
-  protected updateGlobals(tStart: HighPrecisionTime, tEnd: HighPrecisionTime) {
-    const vizTime = new HighPrecisionTimeSpan(
-      tStart,
-      tEnd.sub(tStart).toNumber(),
-    );
-    this.updateVizTime(vizTime);
-  }
-}
diff --git a/ui/src/frontend/drag/inner_drag_strategy.ts b/ui/src/frontend/drag/inner_drag_strategy.ts
deleted file mode 100644
index 61ad694..0000000
--- a/ui/src/frontend/drag/inner_drag_strategy.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2021 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 {TimeScale} from '../../base/time_scale';
-import {DragStrategy, DragStrategyUpdateTimeFn} from './drag_strategy';
-
-export class InnerDragStrategy extends DragStrategy {
-  private dragStartPx = 0;
-
-  constructor(
-    timeScale: TimeScale,
-    private pixelBounds: [number, number],
-    updateVizTime: DragStrategyUpdateTimeFn,
-  ) {
-    super(timeScale, updateVizTime);
-  }
-
-  onDrag(x: number) {
-    const move = x - this.dragStartPx;
-    const tStart = this.map.pxToHpTime(this.pixelBounds[0] + move);
-    const tEnd = this.map.pxToHpTime(this.pixelBounds[1] + move);
-    super.updateGlobals(tStart, tEnd);
-  }
-
-  onDragStart(x: number) {
-    this.dragStartPx = x;
-  }
-}
diff --git a/ui/src/frontend/drag/outer_drag_strategy.ts b/ui/src/frontend/drag/outer_drag_strategy.ts
deleted file mode 100644
index 1374193..0000000
--- a/ui/src/frontend/drag/outer_drag_strategy.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2021 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 {DragStrategy} from './drag_strategy';
-
-export class OuterDragStrategy extends DragStrategy {
-  private dragStartPx = 0;
-
-  onDrag(x: number) {
-    const tStart = this.map.pxToHpTime(Math.min(x, this.dragStartPx));
-    const tEnd = this.map.pxToHpTime(Math.max(x, this.dragStartPx));
-    super.updateGlobals(tStart, tEnd);
-  }
-
-  onDragStart(x: number) {
-    this.dragStartPx = x;
-  }
-}
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 3e27b07..862333d 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -34,6 +34,7 @@
   // Here we rely on the exception message from onCannotGrowMemory function
   if (
     err.message.includes('Cannot enlarge memory') ||
+    err.stack.some((entry) => entry.name.includes('base::AlignedAlloc')) ||
     err.stack.some((entry) => entry.name.includes('OutOfMemoryHandler')) ||
     err.stack.some((entry) => entry.name.includes('_emscripten_resize_heap')) ||
     err.stack.some((entry) => entry.name.includes('sbrk')) ||
@@ -230,7 +231,6 @@
   }
 
   private onUploadCheckboxChange(checked: boolean) {
-    raf.scheduleFullRedraw();
     this.attachTrace = checked;
 
     if (
@@ -242,7 +242,7 @@
       this.uploadStatus = '';
       const uploader = new GcsUploader(this.traceData, {
         onProgress: () => {
-          raf.scheduleFullRedraw('force');
+          raf.scheduleFullRedraw();
           this.uploadStatus = uploader.getEtaString();
           if (uploader.state === 'UPLOADED') {
             this.traceState = 'UPLOADED';
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index 434d549..acf5f6a 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -14,7 +14,6 @@
 
 import m from 'mithril';
 import {Icons} from '../base/semantic_icons';
-import {raf} from '../core/raf_scheduler';
 import {Flow} from '../core/flow_types';
 import {TraceImpl} from '../core/trace_impl';
 
@@ -124,7 +123,6 @@
                   flows.setCategoryVisible(ALL_CATEGORIES, false);
                 }
                 flows.setCategoryVisible(cat, !wasChecked);
-                raf.scheduleFullRedraw();
               },
             },
             wasChecked ? Icons.Checkbox : Icons.BlankCheckbox,
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index acf092a..7051d36 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -23,7 +23,8 @@
   nativeKeyboardLayoutMap,
   NotSupportedError,
 } from '../base/keyboard_layout_map';
-import {KeyMapping} from './viewer_page/pan_and_zoom_handler';
+import {KeyMapping} from './viewer_page/wasd_navigation_handler';
+import {raf} from '../core/raf_scheduler';
 
 export function toggleHelp() {
   AppImpl.instance.analytics.logEvent('User Actions', 'Show help');
@@ -54,7 +55,7 @@
     nativeKeyboardLayoutMap()
       .then((keyMap: KeyboardLayoutMap) => {
         this.keyMap = keyMap;
-        AppImpl.instance.scheduleFullRedraw('force');
+        raf.scheduleFullRedraw();
       })
       .catch((e) => {
         if (
@@ -69,7 +70,7 @@
           // The alternative would be to show key mappings for all keyboard
           // layouts which is not feasible.
           this.keyMap = new EnglishQwertyKeyboardLayoutMap();
-          AppImpl.instance.scheduleFullRedraw('force');
+          raf.scheduleFullRedraw();
         } else {
           // Something unexpected happened. Either the browser doesn't conform
           // to the keyboard API spec, or the keyboard API spec has changed!
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index d03a904..2d0fa7a 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -24,7 +24,6 @@
 import {initLiveReload} from '../core/live_reload';
 import {raf} from '../core/raf_scheduler';
 import {initWasm} from '../trace_processor/wasm_engine_proxy';
-import {setScheduleFullRedraw} from '../widgets/raf';
 import {UiMain} from './ui_main';
 import {initCssConstants} from './css_constants';
 import {registerDebugGlobals} from './debug';
@@ -63,7 +62,7 @@
 });
 
 function routeChange(route: Route) {
-  raf.scheduleFullRedraw('force', () => {
+  raf.scheduleFullRedraw(() => {
     if (route.fragment) {
       // This needs to happen after the next redraw call. It's not enough
       // to use setTimeout(..., 0); since that may occur before the
@@ -147,9 +146,6 @@
     initialRouteArgs: Router.parseUrl(window.location.href).args,
   });
 
-  // Wire up raf for widgets.
-  setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force));
-
   // Load the css. The load is asynchronous and the CSS is not ready by the time
   // appendChild returns.
   const cssLoadPromise = defer<void>();
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 36efe8e..e0562bc 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -333,7 +333,7 @@
   private onMouseDown = (e: Event) => {
     // We need to schedule a redraw manually as this event handler was added
     // manually to the DOM and doesn't use Mithril's auto-redraw system.
-    raf.scheduleFullRedraw('force');
+    raf.scheduleFullRedraw();
 
     // Don't close if the click was within ourselves or our popup.
     if (e.target instanceof Node) {
@@ -349,7 +349,6 @@
 
   private close(attrs: OmniboxAttrs): void {
     const {onClose = () => {}} = attrs;
-    raf.scheduleFullRedraw();
     onClose();
   }
 
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index b69916f..306ee1a 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {assertExists} from '../base/logging';
 import {
   JsonSerialize,
   parseAppState,
@@ -33,6 +32,7 @@
 import {showModal} from '../widgets/modal';
 import {AppImpl} from '../core/app_impl';
 import {CopyableLink} from '../widgets/copyable_link';
+import {TraceImpl} from '../core/trace_impl';
 
 // Permalink serialization has two layers:
 // 1. Serialization of the app state (state_serialization.ts):
@@ -59,19 +59,18 @@
 
 type PermalinkState = z.infer<typeof PERMALINK_SCHEMA>;
 
-export async function createPermalink(): Promise<void> {
-  const hash = await createPermalinkInternal();
+export async function createPermalink(trace: TraceImpl): Promise<void> {
+  const hash = await createPermalinkInternal(trace);
   showPermalinkDialog(hash);
 }
 
 // Returns the file name, not the full url (i.e. the name of the GCS object).
-async function createPermalinkInternal(): Promise<string> {
+async function createPermalinkInternal(trace: TraceImpl): Promise<string> {
   const permalinkData: PermalinkState = {};
 
   // Check if we need to upload the trace file, before serializing the app
   // state.
   let alreadyUploadedUrl = '';
-  const trace = assertExists(AppImpl.instance.trace);
   const traceSource = trace.traceInfo.source;
   let dataToUpload: File | ArrayBuffer | undefined = undefined;
   let traceName = trace.traceInfo.traceTitle || 'trace';
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index da3ddb1..4c23ffb 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -26,7 +26,6 @@
   COUNT_AGGREGATION,
 } from '../core/pivot_table_types';
 import {AreaSelection} from '../public/selection';
-import {raf} from '../core/raf_scheduler';
 import {ColumnType} from '../trace_processor/query_result';
 import {
   aggregationIndex,
@@ -172,7 +171,6 @@
       {
         onclick: () => {
           tree.isCollapsed = !tree.isCollapsed;
-          raf.scheduleFullRedraw();
         },
       },
       m('i.material-icons', tree.isCollapsed ? 'expand_more' : 'expand_less'),
diff --git a/ui/src/frontend/pivot_table_argument_popup.ts b/ui/src/frontend/pivot_table_argument_popup.ts
index a4d67c2..6e99273 100644
--- a/ui/src/frontend/pivot_table_argument_popup.ts
+++ b/ui/src/frontend/pivot_table_argument_popup.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {raf} from '../core/raf_scheduler';
 
 interface ArgumentPopupArgs {
   onArgumentChange: (arg: string) => void;
@@ -26,7 +25,6 @@
   setArgument(attrs: ArgumentPopupArgs, arg: string) {
     this.argument = arg;
     attrs.onArgumentChange(arg);
-    raf.scheduleFullRedraw();
   }
 
   view({attrs}: m.Vnode<ArgumentPopupArgs>): m.Child {
diff --git a/ui/src/frontend/reorderable_cells.ts b/ui/src/frontend/reorderable_cells.ts
index e8e87f9..c327591 100644
--- a/ui/src/frontend/reorderable_cells.ts
+++ b/ui/src/frontend/reorderable_cells.ts
@@ -14,7 +14,6 @@
 
 import m from 'mithril';
 import {DropDirection} from '../core/pivot_table_manager';
-import {raf} from '../core/raf_scheduler';
 
 export interface ReorderableCell {
   content: m.Children;
@@ -74,8 +73,6 @@
             if (e.dataTransfer !== null) {
               e.dataTransfer.setDragImage(placeholderElement, 0, 0);
             }
-
-            raf.scheduleFullRedraw();
           },
           ondragover: (e: DragEvent) => {
             let target = e.target as HTMLElement;
@@ -100,15 +97,8 @@
             const offset = e.clientX - target.getBoundingClientRect().x;
             const newDropDirection =
               offset > target.clientWidth / 2 ? 'right' : 'left';
-            const redraw =
-              newDropDirection !== this.dropDirection ||
-              index !== this.draggingTo;
             this.dropDirection = newDropDirection;
             this.draggingTo = index;
-
-            if (redraw) {
-              raf.scheduleFullRedraw();
-            }
           },
           ondragenter: (e: DragEvent) => {
             this.enterCounters[index]++;
@@ -128,7 +118,6 @@
             }
 
             this.draggingTo = -1;
-            raf.scheduleFullRedraw();
           },
           ondragend: () => {
             if (
@@ -144,7 +133,6 @@
 
             this.draggingFrom = -1;
             this.draggingTo = -1;
-            raf.scheduleFullRedraw();
           },
         },
         cell.content,
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index d8a8f46..a76bc73 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -121,19 +121,12 @@
   downloadUrl(fileName, url);
 }
 
-function highPrecisionTimersAvailable(): boolean {
-  // High precision timers are available either when the page is cross-origin
-  // isolated or when the trace processor is a standalone binary.
-  return (
-    window.crossOriginIsolated ||
-    AppImpl.instance.trace?.engine.mode === 'HTTP_RPC'
-  );
-}
-
 function recordMetatrace(engine: Engine) {
   AppImpl.instance.analytics.logEvent('Trace Actions', 'Record metatrace');
 
-  if (!highPrecisionTimersAvailable()) {
+  const highPrecisionTimersAvailable =
+    window.crossOriginIsolated || engine.mode === 'HTTP_RPC';
+  if (!highPrecisionTimersAvailable) {
     const PROMPT = `High-precision timers are not available to WASM trace processor yet.
 
 Modern browsers restrict high-precision timers to cross-origin-isolated pages.
@@ -352,14 +345,12 @@
 }
 
 export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
-  private _redrawWhileAnimating = new Animation(() =>
-    raf.scheduleFullRedraw('force'),
-  );
+  private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
   private _asyncJobPending = new Set<string>();
   private _sectionExpanded = new Map<string, boolean>();
 
-  constructor() {
-    registerMenuItems();
+  constructor({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
+    registerMenuItems(attrs.trace);
   }
 
   view({attrs}: m.CVnode<OptionalTraceImplAttrs>) {
@@ -430,7 +421,6 @@
         {
           onclick: () => {
             this._sectionExpanded.set(sectionId, !expanded);
-            raf.scheduleFullRedraw();
           },
         },
         m('h1', {title: section.title}, section.title),
@@ -522,10 +512,9 @@
         return; // Don't queue up another action if not yet finished.
       }
       this._asyncJobPending.add(itemId);
-      raf.scheduleFullRedraw();
       res.finally(() => {
         this._asyncJobPending.delete(itemId);
-        raf.scheduleFullRedraw('force');
+        raf.scheduleFullRedraw();
       });
     };
   }
@@ -539,12 +528,11 @@
 let globalItemsRegistered = false;
 const traceItemsRegistered = new WeakSet<TraceImpl>();
 
-function registerMenuItems() {
+function registerMenuItems(trace: TraceImpl | undefined) {
   if (!globalItemsRegistered) {
     globalItemsRegistered = true;
     registerGlobalSidebarEntries();
   }
-  const trace = AppImpl.instance.trace;
   if (trace !== undefined && !traceItemsRegistered.has(trace)) {
     traceItemsRegistered.add(trace);
     registerTraceMenuItems(trace);
diff --git a/ui/src/frontend/trace_share_utils.ts b/ui/src/frontend/trace_share_utils.ts
index cf8f185..4e07b4f 100644
--- a/ui/src/frontend/trace_share_utils.ts
+++ b/ui/src/frontend/trace_share_utils.ts
@@ -61,6 +61,6 @@
   );
   if (result) {
     AppImpl.instance.analytics.logEvent('Trace Actions', 'Create permalink');
-    return await createPermalink();
+    return await createPermalink(trace);
   }
 }
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 33303ab..7f20299 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -22,7 +22,6 @@
   setDurationPrecision,
   setTimestampFormat,
 } from '../core/timestamp_format';
-import {raf} from '../core/raf_scheduler';
 import {Command} from '../public/command';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
@@ -140,7 +139,6 @@
             getName: (x) => x.name,
           });
           result && setTimestampFormat(result.format);
-          raf.scheduleFullRedraw();
         },
       },
       {
@@ -159,7 +157,6 @@
             },
           );
           result && setDurationPrecision(result.format);
-          raf.scheduleFullRedraw();
         },
       },
       {
@@ -171,7 +168,7 @@
       {
         id: 'perfetto.ShareTrace',
         name: 'Share trace',
-        callback: shareTrace,
+        callback: () => shareTrace(trace),
       },
       {
         id: 'perfetto.SearchNext',
@@ -429,12 +426,10 @@
       selectedOptionIndex: omnibox.selectionIndex,
       onSelectedOptionChanged: (index) => {
         omnibox.setSelectionIndex(index);
-        raf.scheduleFullRedraw();
       },
       onInput: (value) => {
         omnibox.setText(value);
         omnibox.setSelectionIndex(0);
-        raf.scheduleFullRedraw();
       },
       onSubmit: (value, _alt) => {
         omnibox.resolvePrompt(value);
@@ -490,12 +485,10 @@
       selectedOptionIndex: omnibox.selectionIndex,
       onSelectedOptionChanged: (index) => {
         omnibox.setSelectionIndex(index);
-        raf.scheduleFullRedraw();
       },
       onInput: (value) => {
         omnibox.setText(value);
         omnibox.setSelectionIndex(0);
-        raf.scheduleFullRedraw();
       },
       onClose: () => {
         if (this.omniboxInputEl) {
@@ -531,7 +524,6 @@
 
       onInput: (value) => {
         AppImpl.instance.omnibox.setText(value);
-        raf.scheduleFullRedraw();
       },
       onSubmit: (query, alt) => {
         const config = {
@@ -539,9 +531,8 @@
           title: alt ? 'Pinned query' : 'Omnibox query',
         };
         const tag = alt ? undefined : 'omnibox_query';
-        const trace = AppImpl.instance.trace;
-        if (trace === undefined) return; // No trace loaded
-        addQueryResultsTab(trace, config, tag);
+        if (this.trace === undefined) return; // No trace loaded
+        addQueryResultsTab(this.trace, config, tag);
       },
       onClose: () => {
         AppImpl.instance.omnibox.setText('');
@@ -549,7 +540,6 @@
           this.omniboxInputEl.blur();
         }
         AppImpl.instance.omnibox.reset();
-        raf.scheduleFullRedraw();
       },
       onGoBack: () => {
         AppImpl.instance.omnibox.reset();
diff --git a/ui/src/frontend/viewer_page/flow_events_renderer.ts b/ui/src/frontend/viewer_page/flow_events_renderer.ts
index f6561ef..682d09c 100644
--- a/ui/src/frontend/viewer_page/flow_events_renderer.ts
+++ b/ui/src/frontend/viewer_page/flow_events_renderer.ts
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {ArrowHeadStyle, drawBezierArrow} from '../../base/canvas/bezier_arrow';
-import {HorizontalBounds, Point2D, Size2D} from '../../base/geom';
+import {ArrowHeadStyle, drawBezierArrow} from '../../base/bezier_arrow';
+import {
+  HorizontalBounds,
+  Point2D,
+  Size2D,
+  VerticalBounds,
+} from '../../base/geom';
 import {TimeScale} from '../../base/time_scale';
 import {Flow} from '../../core/flow_types';
 import {TraceImpl} from '../../core/trace_impl';
 import {TrackNode} from '../../public/workspace';
 import {ALL_CATEGORIES, getFlowCategories} from '../flow_events_panel';
-import {RenderedPanelInfo} from './panel_container';
 
 const TRACK_GROUP_CONNECTION_OFFSET = 5;
 const TRIANGLE_SIZE = 5;
@@ -40,6 +44,11 @@
   | ({kind: 'vertical_edge'} & Point2D)
   | ({kind: 'point'} & Point2D);
 
+export interface TrackInfo {
+  readonly node: TrackNode;
+  readonly verticalBounds: VerticalBounds;
+}
+
 /**
  * Renders the flows overlay on top of the timeline, given the set of panels and
  * a canvas to draw on.
@@ -50,24 +59,25 @@
  * @param trace - The Trace instance, which holds onto the FlowManager.
  * @param ctx - The canvas to draw on.
  * @param size - The size of the canvas.
- * @param panels - A list of panels and their locations on the canvas.
+ * @param tracks - A list of tracks and their vertical positions on the canvas.
+ * @param trackRoot - The root node of the tracks - used to find tracks quickly
+ * by URI.
+ * @param timescale - The current timescale used to convert flow timings into
+ * canvas positions.
+ *
  */
 export function renderFlows(
   trace: TraceImpl,
   ctx: CanvasRenderingContext2D,
   size: Size2D,
-  panels: ReadonlyArray<RenderedPanelInfo>,
+  tracks: ReadonlyArray<TrackInfo>,
   trackRoot: TrackNode,
+  timescale: TimeScale,
 ): void {
-  const timescale = new TimeScale(trace.timeline.visibleWindow, {
-    left: 0,
-    right: size.width,
-  });
-
   // Create an index of track node instances to panels. This doesn't need to be
   // a WeakMap because it's thrown away every render cycle.
-  const panelsByTrackNode = new Map(
-    panels.map((panel) => [panel.panel.trackNode, panel]),
+  const trackInfoByNode = new Map(
+    tracks.map((trackInfo) => [trackInfo.node, trackInfo]),
   );
 
   const drawFlow = (flow: Flow, hue: number) => {
@@ -128,15 +138,17 @@
       return undefined;
     }
 
-    const track = trackRoot.findTrackByUri(trackUri);
+    const track = trackRoot.getTrackByUri(trackUri);
     if (!track) {
       return undefined;
     }
 
-    const trackPanel = panelsByTrackNode.get(track);
+    const trackPanel = trackInfoByNode.get(track);
     if (trackPanel) {
-      const trackRect = trackPanel.rect;
-      const sliceRectRaw = trackPanel.panel.getSliceVerticalBounds?.(depth);
+      const trackRect = trackPanel.verticalBounds;
+      const sliceRectRaw = trace.tracks
+        .getTrack(trackUri)
+        ?.track.getSliceVerticalBounds?.(depth);
       if (sliceRectRaw) {
         const sliceRect = {
           top: sliceRectRaw.top + trackRect.top,
@@ -159,12 +171,12 @@
     } else {
       // If we didn't find a track, it might inside a group, so check for the group
       const containerNode = track.findClosestVisibleAncestor();
-      const groupPanel = panelsByTrackNode.get(containerNode);
+      const groupPanel = trackInfoByNode.get(containerNode);
       if (groupPanel) {
         return {
           kind: 'point',
           x,
-          y: groupPanel.rect.bottom - TRACK_GROUP_CONNECTION_OFFSET,
+          y: groupPanel.verticalBounds.bottom - TRACK_GROUP_CONNECTION_OFFSET,
         };
       }
     }
diff --git a/ui/src/frontend/viewer_page/notes_panel.ts b/ui/src/frontend/viewer_page/notes_panel.ts
index 49c9576..be0b63c 100644
--- a/ui/src/frontend/viewer_page/notes_panel.ts
+++ b/ui/src/frontend/viewer_page/notes_panel.ts
@@ -17,15 +17,17 @@
 import {currentTargetOffset} from '../../base/dom_utils';
 import {Size2D} from '../../base/geom';
 import {assertUnreachable} from '../../base/logging';
+import {Icons} from '../../base/semantic_icons';
 import {TimeScale} from '../../base/time_scale';
 import {randomColor} from '../../components/colorizer';
 import {raf} from '../../core/raf_scheduler';
 import {TraceImpl} from '../../core/trace_impl';
 import {Note, SpanNote} from '../../public/note';
 import {Button, ButtonBar} from '../../widgets/button';
+import {MenuItem, PopupMenu2} from '../../widgets/menu';
+import {Select} from '../../widgets/select';
 import {TRACK_SHELL_WIDTH} from '../css_constants';
 import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
 
 const FLAG_WIDTH = 16;
 const AREA_TRIANGLE_WIDTH = 10;
@@ -48,13 +50,12 @@
   }
 }
 
-export class NotesPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = false;
+export class NotesPanel {
   private readonly trace: TraceImpl;
   private timescale?: TimeScale; // The timescale from the last render()
   private hoveredX: null | number = null;
   private mouseDragging = false;
+  readonly height = 20;
 
   constructor(trace: TraceImpl) {
     this.trace = trace;
@@ -65,9 +66,12 @@
       (n) => n.collapsed,
     );
 
+    const workspaces = this.trace.workspaces;
+
     return m(
-      '.notes-panel',
+      '',
       {
+        style: {height: `${this.height}px`},
         onmousedown: () => {
           // If the user clicks & drags, very likely they just want to measure
           // the time horizontally, not set a flag. This debouncing is done to
@@ -98,7 +102,7 @@
       },
       m(
         ButtonBar,
-        {className: 'pf-toolbar'},
+        {className: 'pf-timeline-toolbar'},
         m(Button, {
           onclick: (e: Event) => {
             e.preventDefault();
@@ -122,12 +126,77 @@
             this.trace.workspace.pinnedTracks.forEach((t) =>
               this.trace.workspace.unpinTrack(t),
             );
-            raf.scheduleFullRedraw();
           },
           title: 'Clear all pinned tracks',
           icon: 'clear_all',
           compact: true,
         }),
+        m(
+          Select,
+          {
+            className: 'pf-timeline-toolbar__workspace-selector',
+            onchange: async (e) => {
+              const value = (e.target as HTMLSelectElement).value;
+              if (value === 'new-workspace') {
+                const ws =
+                  workspaces.createEmptyWorkspace('Untitled Workspace');
+                workspaces.switchWorkspace(ws);
+              } else {
+                const ws = workspaces.all.find(({id}) => id === value);
+                ws && this.trace?.workspaces.switchWorkspace(ws);
+              }
+            },
+          },
+          workspaces.all
+            .map((ws) => {
+              return m('option', {
+                value: `${ws.id}`,
+                label: ws.title,
+                selected: ws === this.trace?.workspace,
+              });
+            })
+            .concat([
+              m('option', {
+                value: 'new-workspace',
+                label: 'New workspace...',
+              }),
+            ]),
+        ),
+        m(
+          PopupMenu2,
+          {
+            trigger: m(Button, {
+              icon: 'more_vert',
+              title: 'Workspace options',
+              compact: true,
+            }),
+          },
+          m(MenuItem, {
+            icon: Icons.Delete,
+            label: 'Delete current workspace',
+            disabled:
+              workspaces.currentWorkspace === workspaces.defaultWorkspace,
+            onclick: () => {
+              workspaces.removeWorkspace(workspaces.currentWorkspace);
+              raf.scheduleFullRedraw();
+            },
+          }),
+          m(MenuItem, {
+            icon: 'edit',
+            label: 'Rename current workspace',
+            disabled:
+              workspaces.currentWorkspace === workspaces.defaultWorkspace,
+            onclick: async () => {
+              const newName = await this.trace.omnibox.prompt(
+                'Enter a new name...',
+              );
+              if (newName) {
+                workspaces.currentWorkspace.title = newName;
+              }
+              raf.scheduleFullRedraw();
+            },
+          }),
+        ),
         // TODO(stevegolton): Re-introduce this when we fix track filtering
         // m(TextInput, {
         //   placeholder: 'Filter tracks...',
@@ -155,7 +224,7 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     ctx.fillStyle = '#999';
-    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+    ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
 
     const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
 
diff --git a/ui/src/frontend/viewer_page/overview_timeline_panel.ts b/ui/src/frontend/viewer_page/overview_timeline_panel.ts
index 4adedf1..0095019 100644
--- a/ui/src/frontend/viewer_page/overview_timeline_panel.ts
+++ b/ui/src/frontend/viewer_page/overview_timeline_panel.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2018 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.
@@ -13,134 +13,117 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {DragGestureHandler} from '../../base/drag_gesture_handler';
-import {Size2D} from '../../base/geom';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
 import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
-import {assertUnreachable} from '../../base/logging';
+import {assertExists, assertUnreachable} from '../../base/logging';
 import {Duration, duration, Time, time, TimeSpan} from '../../base/time';
 import {TimeScale} from '../../base/time_scale';
 import {getOrCreate} from '../../base/utils';
+import {ZonedInteractionHandler} from '../../base/zoned_interaction_handler';
 import {colorForCpu} from '../../components/colorizer';
 import {raf} from '../../core/raf_scheduler';
 import {timestampFormat} from '../../core/timestamp_format';
 import {TraceImpl} from '../../core/trace_impl';
 import {TimestampFormat} from '../../public/timeline';
 import {LONG, NUM} from '../../trace_processor/query_result';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
 import {
   OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
   TRACK_SHELL_WIDTH,
 } from '../css_constants';
-import {BorderDragStrategy} from '../drag/border_drag_strategy';
-import {DragStrategy} from '../drag/drag_strategy';
-import {InnerDragStrategy} from '../drag/inner_drag_strategy';
-import {OuterDragStrategy} from '../drag/outer_drag_strategy';
 import {
   generateTicks,
   getMaxMajorTicks,
   MIN_PX_PER_STEP,
   TickType,
 } from './gridline_helper';
-import {Panel} from './panel_container';
+
+const HANDLE_SIZE_PX = 5;
+
+export interface OverviewTimelineAttrs {
+  readonly trace: TraceImpl;
+  readonly className?: string;
+}
 
 const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
 
-export class OverviewTimelinePanel implements Panel {
-  private static HANDLE_SIZE_PX = 5;
-  readonly kind = 'panel';
-  readonly selectable = false;
-  private width = 0;
-  private gesture?: DragGestureHandler;
-  private timeScale?: TimeScale;
-  private dragStrategy?: DragStrategy;
-  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
+export class OverviewTimeline
+  implements m.ClassComponent<OverviewTimelineAttrs>
+{
   private readonly overviewData: OverviewDataLoader;
+  private readonly trash = new DisposableStack();
+  private interactions?: ZonedInteractionHandler;
 
-  constructor(private trace: TraceImpl) {
+  constructor({attrs}: m.CVnode<OverviewTimelineAttrs>) {
     this.overviewData = getOrCreate(
       tracesData,
-      trace,
-      () => new OverviewDataLoader(trace),
+      attrs.trace,
+      () => new OverviewDataLoader(attrs.trace),
     );
   }
 
-  // Must explicitly type now; arguments types are no longer auto-inferred.
-  // https://github.com/Microsoft/TypeScript/issues/1373
-  onupdate({dom}: m.CVnodeDOM) {
-    this.width = dom.getBoundingClientRect().width;
-    const traceTime = this.trace.traceInfo;
-    if (this.width > TRACK_SHELL_WIDTH) {
-      const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
-      const hpTraceTime = HighPrecisionTimeSpan.fromTime(
-        traceTime.start,
-        traceTime.end,
-      );
-      this.timeScale = new TimeScale(hpTraceTime, pxBounds);
-      if (this.gesture === undefined) {
-        this.gesture = new DragGestureHandler(
-          dom as HTMLElement,
-          this.onDrag.bind(this),
-          this.onDragStart.bind(this),
-          this.onDragEnd.bind(this),
-        );
-      }
-    } else {
-      this.timeScale = undefined;
-    }
-  }
-
-  oncreate(vnode: m.CVnodeDOM) {
-    this.onupdate(vnode);
-    (vnode.dom as HTMLElement).addEventListener(
-      'mousemove',
-      this.boundOnMouseMove,
+  view({attrs}: m.CVnode<OverviewTimelineAttrs>) {
+    return m(
+      VirtualOverlayCanvas,
+      {
+        raf: attrs.trace.raf,
+        className: attrs.className,
+        onCanvasRedraw: ({ctx, virtualCanvasSize}) => {
+          this.renderCanvas(attrs.trace, ctx, virtualCanvasSize);
+        },
+      },
+      m('.pf-overview-timeline'),
     );
   }
 
-  onremove({dom}: m.CVnodeDOM) {
-    if (this.gesture) {
-      this.gesture[Symbol.dispose]();
-      this.gesture = undefined;
-    }
-    (dom as HTMLElement).removeEventListener(
-      'mousemove',
-      this.boundOnMouseMove,
+  oncreate({dom}: m.VnodeDOM<OverviewTimelineAttrs, this>) {
+    this.interactions = new ZonedInteractionHandler(toHTMLElement(dom));
+    this.trash.use(this.interactions);
+  }
+
+  onremove(_: m.VnodeDOM<OverviewTimelineAttrs, this>) {
+    this.trash.dispose();
+  }
+
+  private renderCanvas(
+    trace: TraceImpl,
+    ctx: CanvasRenderingContext2D,
+    size: Size2D,
+  ) {
+    if (size.width <= TRACK_SHELL_WIDTH) return;
+
+    const traceTime = trace.traceInfo;
+    const pxBounds = {left: TRACK_SHELL_WIDTH, right: size.width};
+    const hpTraceTime = HighPrecisionTimeSpan.fromTime(
+      traceTime.start,
+      traceTime.end,
     );
-  }
-
-  render(): m.Children {
-    return m('.overview-timeline', {
-      oncreate: (vnode) => this.oncreate(vnode),
-      onupdate: (vnode) => this.onupdate(vnode),
-      onremove: (vnode) => this.onremove(vnode),
-    });
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
-    if (this.width === undefined) return;
-    if (this.timeScale === undefined) return;
+    const timescale = new TimeScale(hpTraceTime, pxBounds);
 
     const headerHeight = 20;
     const tracksHeight = size.height - headerHeight;
     const traceContext = new TimeSpan(
-      this.trace.traceInfo.start,
-      this.trace.traceInfo.end,
+      trace.traceInfo.start,
+      trace.traceInfo.end,
     );
 
     if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
-      const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
-      const offset = this.trace.timeline.timestampOffset();
+      const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
+      const offset = trace.timeline.timestampOffset();
       const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
 
       // Draw time labels
       ctx.font = '10px Roboto Condensed';
       ctx.fillStyle = '#999';
       for (const {type, time} of tickGen) {
-        const xPos = Math.floor(this.timeScale.timeToPx(time));
+        const xPos = Math.floor(timescale.timeToPx(time));
         if (xPos <= 0) continue;
-        if (xPos > this.width) break;
+        if (xPos > size.width) break;
         if (type === TickType.MAJOR) {
           ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
-          const domainTime = this.trace.timeline.toDomainTime(time);
+          const domainTime = trace.timeline.toDomainTime(time);
           renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
         } else if (type == TickType.MEDIUM) {
           ctx.fillRect(xPos - 1, 0, 1, 8);
@@ -159,8 +142,8 @@
       for (const key of overviewData.keys()) {
         const loads = overviewData.get(key)!;
         for (let i = 0; i < loads.length; i++) {
-          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
-          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
+          const xStart = Math.floor(timescale.timeToPx(loads[i].start));
+          const xEnd = Math.ceil(timescale.timeToPx(loads[i].end));
           const yOff = Math.floor(headerHeight + y * trackHeight);
           const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
           const color = colorForCpu(y).setHSL({s: 50, l: lightness});
@@ -173,10 +156,15 @@
 
     // Draw bottom border.
     ctx.fillStyle = '#dadada';
-    ctx.fillRect(0, size.height - 1, this.width, 1);
+    ctx.fillRect(0, size.height - 1, size.width, 1);
 
     // Draw semi-opaque rects that occlude the non-visible time range.
-    const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
+    const {left, right} = timescale.hpTimeSpanToPxSpan(
+      trace.timeline.visibleWindow,
+    );
+
+    const vizStartPx = Math.floor(left);
+    const vizEndPx = Math.ceil(right);
 
     ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
     ctx.fillRect(
@@ -185,14 +173,14 @@
       vizStartPx - TRACK_SHELL_WIDTH,
       tracksHeight,
     );
-    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
+    ctx.fillRect(vizEndPx, headerHeight, size.width - vizEndPx, tracksHeight);
 
     // Draw brushes.
     ctx.fillStyle = '#999';
     ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
     ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
 
-    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
+    const hbarWidth = HANDLE_SIZE_PX;
     const hbarHeight = tracksHeight * 0.4;
     // Draw handlebar
     ctx.fillRect(
@@ -207,73 +195,79 @@
       hbarWidth,
       hbarHeight,
     );
-  }
 
-  private onMouseMove(e: MouseEvent) {
-    if (this.gesture === undefined || this.gesture.isDragging) {
-      return;
-    }
-    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
-  }
-
-  private chooseCursor(x: number) {
-    if (this.timeScale === undefined) return 'default';
-    const [startBound, endBound] = this.extractBounds(this.timeScale);
-    if (
-      OverviewTimelinePanel.inBorderRange(x, startBound) ||
-      OverviewTimelinePanel.inBorderRange(x, endBound)
-    ) {
-      return 'ew-resize';
-    } else if (x < TRACK_SHELL_WIDTH) {
-      return 'default';
-    } else if (x < startBound || endBound < x) {
-      return 'crosshair';
-    } else {
-      return 'all-scroll';
-    }
-  }
-
-  onDrag(x: number) {
-    if (this.dragStrategy === undefined) return;
-    this.dragStrategy.onDrag(x);
-  }
-
-  onDragStart(x: number) {
-    if (this.timeScale === undefined) return;
-
-    const cb = (vizTime: HighPrecisionTimeSpan) => {
-      this.trace.timeline.updateVisibleTimeHP(vizTime);
-      raf.scheduleCanvasRedraw();
-    };
-    const pixelBounds = this.extractBounds(this.timeScale);
-    const timeScale = this.timeScale;
-    if (
-      OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
-      OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
-    ) {
-      this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb);
-    } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
-      this.dragStrategy = new OuterDragStrategy(timeScale, cb);
-    } else {
-      this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb);
-    }
-    this.dragStrategy.onDragStart(x);
-  }
-
-  onDragEnd() {
-    this.dragStrategy = undefined;
-  }
-
-  private extractBounds(timeScale: TimeScale): [number, number] {
-    const vizTime = this.trace.timeline.visibleWindow;
-    return [
-      Math.floor(timeScale.hpTimeToPx(vizTime.start)),
-      Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
-    ];
-  }
-
-  private static inBorderRange(a: number, b: number): boolean {
-    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
+    assertExists(this.interactions).update([
+      {
+        id: 'left-handle',
+        area: Rect2D.fromPointAndSize({
+          x: vizStartPx - Math.floor(hbarWidth / 2) - 1,
+          y: 0,
+          width: hbarWidth,
+          height: size.height,
+        }),
+        cursor: 'col-resize',
+        drag: {
+          cursorWhileDragging: 'col-resize',
+          onDrag: (event) => {
+            const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+            trace.timeline.moveStart(delta);
+          },
+        },
+      },
+      {
+        id: 'right-handle',
+        area: Rect2D.fromPointAndSize({
+          x: vizEndPx - Math.floor(hbarWidth / 2) - 1,
+          y: 0,
+          width: hbarWidth,
+          height: size.height,
+        }),
+        cursor: 'col-resize',
+        drag: {
+          cursorWhileDragging: 'col-resize',
+          onDrag: (event) => {
+            const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+            trace.timeline.moveEnd(delta);
+          },
+        },
+      },
+      {
+        id: 'drag',
+        area: new Rect2D({
+          left: vizStartPx,
+          right: vizEndPx,
+          top: 0,
+          bottom: size.height,
+        }),
+        cursor: 'grab',
+        drag: {
+          cursorWhileDragging: 'grabbing',
+          onDrag: (event) => {
+            const delta = timescale.pxToDuration(event.deltaSinceLastEvent.x);
+            trace.timeline.panVisibleWindow(delta);
+          },
+        },
+      },
+      {
+        id: 'select',
+        area: new Rect2D({
+          left: TRACK_SHELL_WIDTH,
+          right: size.width,
+          top: 0,
+          bottom: size.height,
+        }),
+        cursor: 'text',
+        drag: {
+          cursorWhileDragging: 'text',
+          onDrag: (event) => {
+            const span = timescale.pxSpanToHpTimeSpan(
+              Rect2D.fromPoints(event.dragStart, event.dragCurrent),
+            );
+            trace.timeline.updateVisibleTimeHP(span);
+          },
+        },
+      },
+    ]);
   }
 }
 
diff --git a/ui/src/frontend/viewer_page/panel_container.ts b/ui/src/frontend/viewer_page/panel_container.ts
deleted file mode 100644
index 2e2a253..0000000
--- a/ui/src/frontend/viewer_page/panel_container.ts
+++ /dev/null
@@ -1,553 +0,0 @@
-// Copyright (C) 2018 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 {canvasClip} from '../../base/canvas_utils';
-import {DisposableStack} from '../../base/disposable_stack';
-import {findRef, toHTMLElement} from '../../base/dom_utils';
-import {Bounds2D, Size2D, VerticalBounds} from '../../base/geom';
-import {assertExists, assertFalse} from '../../base/logging';
-import {SimpleResizeObserver} from '../../base/resize_observer';
-import {TimeScale} from '../../base/time_scale';
-import {VirtualCanvas} from '../../base/virtual_canvas';
-import {
-  PerfStats,
-  PerfStatsContainer,
-  runningStatStr,
-} from '../../core/perf_stats';
-import {raf} from '../../core/raf_scheduler';
-import {TraceImpl, TraceImplAttrs} from '../../core/trace_impl';
-import {TrackNode} from '../../public/workspace';
-import {HTMLAttrs} from '../../widgets/common';
-import {SELECTION_STROKE_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-
-const CANVAS_OVERDRAW_PX = 300;
-const CANVAS_TOLERANCE_PX = 100;
-
-export interface Panel {
-  readonly kind: 'panel';
-  render(): m.Children;
-  readonly selectable: boolean;
-  // TODO(stevegolton): Remove this - panel container should know nothing of
-  // tracks!
-  readonly trackNode?: TrackNode;
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void;
-  getSliceVerticalBounds?(depth: number): VerticalBounds | undefined;
-}
-
-export interface PanelGroup {
-  readonly kind: 'group';
-  readonly collapsed: boolean;
-  readonly header?: Panel;
-  readonly topOffsetPx: number;
-  readonly sticky: boolean;
-  readonly childPanels: PanelOrGroup[];
-}
-
-export type PanelOrGroup = Panel | PanelGroup;
-
-export interface PanelContainerAttrs extends TraceImplAttrs {
-  panels: PanelOrGroup[];
-  className?: string;
-  selectedYRange: VerticalBounds | undefined;
-
-  onPanelStackResize?: (width: number, height: number) => void;
-
-  // Called after all panels have been rendered to the canvas, to give the
-  // caller the opportunity to render an overlay on top of the panels.
-  renderOverlay?(
-    ctx: CanvasRenderingContext2D,
-    size: Size2D,
-    panels: ReadonlyArray<RenderedPanelInfo>,
-  ): void;
-
-  // Called before the panels are rendered
-  renderUnderlay?(ctx: CanvasRenderingContext2D, size: Size2D): void;
-}
-
-interface PanelInfo {
-  trackNode?: TrackNode; // Can be undefined for singleton panels.
-  panel: Panel;
-  height: number;
-  width: number;
-  clientX: number;
-  clientY: number;
-  absY: number;
-}
-
-export interface RenderedPanelInfo {
-  panel: Panel;
-  rect: Bounds2D;
-}
-
-export class PanelContainer
-  implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer
-{
-  private readonly trace: TraceImpl;
-  private attrs: PanelContainerAttrs;
-
-  // Updated every render cycle in the view() hook
-  private panelById = new Map<string, Panel>();
-
-  // Updated every render cycle in the oncreate/onupdate hook
-  private panelInfos: PanelInfo[] = [];
-
-  private perfStatsEnabled = false;
-  private panelPerfStats = new WeakMap<Panel, PerfStats>();
-  private perfStats = {
-    totalPanels: 0,
-    panelsOnCanvas: 0,
-    renderStats: new PerfStats(10),
-  };
-
-  private ctx?: CanvasRenderingContext2D;
-
-  private readonly trash = new DisposableStack();
-
-  private readonly OVERLAY_REF = 'overlay';
-  private readonly PANEL_STACK_REF = 'panel-stack';
-
-  constructor({attrs}: m.CVnode<PanelContainerAttrs>) {
-    this.attrs = attrs;
-    this.trace = attrs.trace;
-    this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas()));
-    this.trash.use(attrs.trace.perfDebugging.addContainer(this));
-  }
-
-  getPanelsInRegion(
-    startX: number,
-    endX: number,
-    startY: number,
-    endY: number,
-  ): Panel[] {
-    const minX = Math.min(startX, endX);
-    const maxX = Math.max(startX, endX);
-    const minY = Math.min(startY, endY);
-    const maxY = Math.max(startY, endY);
-    const panels: Panel[] = [];
-    for (let i = 0; i < this.panelInfos.length; i++) {
-      const pos = this.panelInfos[i];
-      const realPosX = pos.clientX - TRACK_SHELL_WIDTH;
-      if (
-        realPosX + pos.width >= minX &&
-        realPosX <= maxX &&
-        pos.absY + pos.height >= minY &&
-        pos.absY <= maxY &&
-        pos.panel.selectable
-      ) {
-        panels.push(pos.panel);
-      }
-    }
-    return panels;
-  }
-
-  // This finds the tracks covered by the in-progress area selection. When
-  // editing areaY is not set, so this will not be used.
-  handleAreaSelection() {
-    const {selectedYRange} = this.attrs;
-    const area = this.trace.timeline.selectedArea;
-    if (
-      area === undefined ||
-      selectedYRange === undefined ||
-      this.panelInfos.length === 0
-    ) {
-      return;
-    }
-
-    // TODO(stevegolton): We shouldn't know anything about visible time scale
-    // right now, that's a job for our parent, but we can put one together so we
-    // don't have to refactor this entire bit right now...
-
-    const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, {
-      left: 0,
-      right: this.virtualCanvas!.size.width - TRACK_SHELL_WIDTH,
-    });
-
-    // The Y value is given from the top of the pan and zoom region, we want it
-    // from the top of the panel container. The parent offset corrects that.
-    const panels = this.getPanelsInRegion(
-      visibleTimeScale.timeToPx(area.start),
-      visibleTimeScale.timeToPx(area.end),
-      selectedYRange.top,
-      selectedYRange.bottom,
-    );
-
-    // Get the track ids from the panels.
-    const trackUris: string[] = [];
-    for (const panel of panels) {
-      if (panel.trackNode) {
-        if (panel.trackNode.isSummary) {
-          const groupNode = panel.trackNode;
-          // Select a track group and all child tracks if it is collapsed
-          if (groupNode.collapsed) {
-            for (const track of groupNode.flatTracks) {
-              track.uri && trackUris.push(track.uri);
-            }
-          }
-        } else {
-          panel.trackNode.uri && trackUris.push(panel.trackNode.uri);
-        }
-      }
-    }
-    this.trace.timeline.selectArea(area.start, area.end, trackUris);
-  }
-
-  private virtualCanvas?: VirtualCanvas;
-
-  oncreate(vnode: m.CVnodeDOM<PanelContainerAttrs>) {
-    const {dom, attrs} = vnode;
-
-    const overlayElement = toHTMLElement(
-      assertExists(findRef(dom, this.OVERLAY_REF)),
-    );
-
-    const virtualCanvas = new VirtualCanvas(overlayElement, dom, {
-      overdrawAxes: 'y',
-      overdrawPx: CANVAS_OVERDRAW_PX,
-      tolerancePx: CANVAS_TOLERANCE_PX,
-    });
-    this.trash.use(virtualCanvas);
-    this.virtualCanvas = virtualCanvas;
-
-    const ctx = virtualCanvas.canvasElement.getContext('2d');
-    if (!ctx) {
-      throw Error('Cannot create canvas context');
-    }
-    this.ctx = ctx;
-
-    virtualCanvas.setCanvasResizeListener((canvas, width, height) => {
-      const dpr = window.devicePixelRatio;
-      canvas.width = width * dpr;
-      canvas.height = height * dpr;
-    });
-
-    virtualCanvas.setLayoutShiftListener(() => {
-      this.renderCanvas();
-    });
-
-    this.onupdate(vnode);
-
-    const panelStackElement = toHTMLElement(
-      assertExists(findRef(dom, this.PANEL_STACK_REF)),
-    );
-
-    // Listen for when the panel stack changes size
-    this.trash.use(
-      new SimpleResizeObserver(panelStackElement, () => {
-        attrs.onPanelStackResize?.(
-          panelStackElement.clientWidth,
-          panelStackElement.clientHeight,
-        );
-      }),
-    );
-  }
-
-  onremove() {
-    this.trash.dispose();
-  }
-
-  renderPanel(node: Panel, panelId: string, htmlAttrs?: HTMLAttrs): m.Vnode {
-    assertFalse(this.panelById.has(panelId));
-    this.panelById.set(panelId, node);
-    return m(
-      `.pf-panel`,
-      {...htmlAttrs, 'data-panel-id': panelId},
-      node.render(),
-    );
-  }
-
-  // Render a tree of panels into one vnode. Argument `path` is used to build
-  // `key` attribute for intermediate tree vnodes: otherwise Mithril internals
-  // will complain about keyed and non-keyed vnodes mixed together.
-  renderTree(node: PanelOrGroup, panelId: string): m.Vnode {
-    if (node.kind === 'group') {
-      const style = {
-        position: 'sticky',
-        top: `${node.topOffsetPx}px`,
-        zIndex: `${2000 - node.topOffsetPx}`,
-      };
-      return m(
-        'div.pf-panel-group',
-        node.header &&
-          this.renderPanel(node.header, `${panelId}-header`, {
-            style: !node.collapsed && node.sticky ? style : {},
-          }),
-        ...node.childPanels.map((child, index) =>
-          this.renderTree(child, `${panelId}-${index}`),
-        ),
-      );
-    }
-    return this.renderPanel(node, panelId);
-  }
-
-  view({attrs}: m.CVnode<PanelContainerAttrs>) {
-    this.attrs = attrs;
-    this.panelById.clear();
-    const children = attrs.panels.map((panel, index) =>
-      this.renderTree(panel, `${index}`),
-    );
-
-    return m(
-      '.pf-panel-container',
-      {className: attrs.className},
-      m(
-        '.pf-panel-stack',
-        {ref: this.PANEL_STACK_REF},
-        m('.pf-overlay', {ref: this.OVERLAY_REF}),
-        children,
-      ),
-    );
-  }
-
-  onupdate({dom}: m.CVnodeDOM<PanelContainerAttrs>) {
-    this.readPanelRectsFromDom(dom);
-  }
-
-  private readPanelRectsFromDom(dom: Element): void {
-    this.panelInfos = [];
-
-    const panel = dom.querySelectorAll('.pf-panel');
-    const panels = assertExists(findRef(dom, this.PANEL_STACK_REF));
-    const {top} = panels.getBoundingClientRect();
-    panel.forEach((panelElement) => {
-      const panelHTMLElement = toHTMLElement(panelElement);
-      const panelId = assertExists(panelHTMLElement.dataset.panelId);
-      const panel = assertExists(this.panelById.get(panelId));
-
-      // NOTE: the id can be undefined for singletons like overview timeline.
-      const rect = panelElement.getBoundingClientRect();
-      this.panelInfos.push({
-        trackNode: panel.trackNode,
-        height: rect.height,
-        width: rect.width,
-        clientX: rect.x,
-        clientY: rect.y,
-        absY: rect.y - top,
-        panel,
-      });
-    });
-  }
-
-  private renderCanvas() {
-    if (!this.ctx) return;
-    if (!this.virtualCanvas) return;
-
-    const ctx = this.ctx;
-    const vc = this.virtualCanvas;
-    const redrawStart = performance.now();
-
-    ctx.resetTransform();
-    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-
-    const dpr = window.devicePixelRatio;
-    ctx.scale(dpr, dpr);
-    ctx.translate(-vc.canvasRect.left, -vc.canvasRect.top);
-
-    this.handleAreaSelection();
-
-    const totalRenderedPanels = this.renderPanels(ctx, vc);
-    this.drawTopLayerOnCanvas(ctx, vc);
-
-    // Collect performance as the last thing we do.
-    const redrawDur = performance.now() - redrawStart;
-    this.updatePerfStats(
-      redrawDur,
-      this.panelInfos.length,
-      totalRenderedPanels,
-    );
-  }
-
-  private renderPanels(
-    ctx: CanvasRenderingContext2D,
-    vc: VirtualCanvas,
-  ): number {
-    this.attrs.renderUnderlay?.(ctx, vc.size);
-
-    let panelTop = 0;
-    let totalOnCanvas = 0;
-
-    const renderedPanels = Array<RenderedPanelInfo>();
-
-    for (let i = 0; i < this.panelInfos.length; i++) {
-      const {
-        panel,
-        width: panelWidth,
-        height: panelHeight,
-      } = this.panelInfos[i];
-
-      const panelRect = {
-        left: 0,
-        top: panelTop,
-        bottom: panelTop + panelHeight,
-        right: panelWidth,
-      };
-      const panelSize = {width: panelWidth, height: panelHeight};
-
-      if (vc.overlapsCanvas(panelRect)) {
-        totalOnCanvas++;
-
-        ctx.save();
-        ctx.translate(0, panelTop);
-        canvasClip(ctx, 0, 0, panelWidth, panelHeight);
-        const beforeRender = performance.now();
-        panel.renderCanvas(ctx, panelSize);
-        this.updatePanelStats(
-          i,
-          panel,
-          performance.now() - beforeRender,
-          ctx,
-          panelSize,
-        );
-        ctx.restore();
-      }
-
-      renderedPanels.push({
-        panel,
-        rect: {
-          top: panelTop,
-          bottom: panelTop + panelHeight,
-          left: 0,
-          right: panelWidth,
-        },
-      });
-
-      panelTop += panelHeight;
-    }
-
-    this.attrs.renderOverlay?.(ctx, vc.size, renderedPanels);
-
-    return totalOnCanvas;
-  }
-
-  // The panels each draw on the canvas but some details need to be drawn across
-  // the whole canvas rather than per panel.
-  private drawTopLayerOnCanvas(
-    ctx: CanvasRenderingContext2D,
-    vc: VirtualCanvas,
-  ): void {
-    const {selectedYRange} = this.attrs;
-    const area = this.trace.timeline.selectedArea;
-    if (area === undefined || selectedYRange === undefined) {
-      return;
-    }
-    if (this.panelInfos.length === 0 || area.trackUris.length === 0) {
-      return;
-    }
-
-    // Find the minY and maxY of the selected tracks in this panel container.
-    let selectedTracksMinY = selectedYRange.top;
-    let selectedTracksMaxY = selectedYRange.bottom;
-    for (let i = 0; i < this.panelInfos.length; i++) {
-      const trackUri = this.panelInfos[i].trackNode?.uri;
-      if (trackUri && area.trackUris.includes(trackUri)) {
-        selectedTracksMinY = Math.min(
-          selectedTracksMinY,
-          this.panelInfos[i].absY,
-        );
-        selectedTracksMaxY = Math.max(
-          selectedTracksMaxY,
-          this.panelInfos[i].absY + this.panelInfos[i].height,
-        );
-      }
-    }
-
-    // TODO(stevegolton): We shouldn't know anything about visible time scale
-    // right now, that's a job for our parent, but we can put one together so we
-    // don't have to refactor this entire bit right now...
-
-    const visibleTimeScale = new TimeScale(this.trace.timeline.visibleWindow, {
-      left: 0,
-      right: vc.size.width - TRACK_SHELL_WIDTH,
-    });
-
-    const startX = visibleTimeScale.timeToPx(area.start);
-    const endX = visibleTimeScale.timeToPx(area.end);
-    ctx.save();
-    ctx.strokeStyle = SELECTION_STROKE_COLOR;
-    ctx.lineWidth = 1;
-
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
-
-    // Clip off any drawing happening outside the bounds of the timeline area
-    canvasClip(ctx, 0, 0, vc.size.width - TRACK_SHELL_WIDTH, vc.size.height);
-
-    ctx.strokeRect(
-      startX,
-      selectedTracksMaxY,
-      endX - startX,
-      selectedTracksMinY - selectedTracksMaxY,
-    );
-    ctx.restore();
-  }
-
-  private updatePanelStats(
-    panelIndex: number,
-    panel: Panel,
-    renderTime: number,
-    ctx: CanvasRenderingContext2D,
-    size: Size2D,
-  ) {
-    if (!this.perfStatsEnabled) return;
-    let renderStats = this.panelPerfStats.get(panel);
-    if (renderStats === undefined) {
-      renderStats = new PerfStats();
-      this.panelPerfStats.set(panel, renderStats);
-    }
-    renderStats.addValue(renderTime);
-
-    // Draw a green box around the whole panel
-    ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)';
-    const lineWidth = 1;
-    ctx.lineWidth = lineWidth;
-    ctx.strokeRect(
-      lineWidth / 2,
-      lineWidth / 2,
-      size.width - lineWidth,
-      size.height - lineWidth,
-    );
-
-    const statW = 300;
-    ctx.fillStyle = 'hsl(97, 100%, 96%)';
-    ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
-    ctx.fillStyle = 'hsla(122, 77%, 22%)';
-    const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
-    ctx.fillText(statStr, size.width - statW, size.height - 10);
-  }
-
-  private updatePerfStats(
-    renderTime: number,
-    totalPanels: number,
-    panelsOnCanvas: number,
-  ) {
-    if (!this.perfStatsEnabled) return;
-    this.perfStats.renderStats.addValue(renderTime);
-    this.perfStats.totalPanels = totalPanels;
-    this.perfStats.panelsOnCanvas = panelsOnCanvas;
-  }
-
-  setPerfStatsEnabled(enable: boolean): void {
-    this.perfStatsEnabled = enable;
-  }
-
-  renderPerfStats() {
-    return [
-      m(
-        'div',
-        `${this.perfStats.totalPanels} panels, ` +
-          `${this.perfStats.panelsOnCanvas} on canvas.`,
-      ),
-      m('div', runningStatStr(this.perfStats.renderStats)),
-    ];
-  }
-}
diff --git a/ui/src/frontend/viewer_page/tickmark_panel.ts b/ui/src/frontend/viewer_page/tickmark_panel.ts
index 8b56938..2dc8bd3 100644
--- a/ui/src/frontend/viewer_page/tickmark_panel.ts
+++ b/ui/src/frontend/viewer_page/tickmark_panel.ts
@@ -20,7 +20,6 @@
 import {TraceImpl} from '../../core/trace_impl';
 import {TRACK_SHELL_WIDTH} from '../css_constants';
 import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
 import {SearchOverviewTrack} from './search_overview_track';
 
 // We want to create the overview track only once per trace, but this
@@ -30,10 +29,9 @@
 const trackTraceMap = new WeakMap<TraceImpl, SearchOverviewTrack>();
 
 // This is used to display the summary of search results.
-export class TickmarkPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = false;
+export class TickmarkPanel {
   private searchOverviewTrack: SearchOverviewTrack;
+  readonly height = 5;
 
   constructor(private readonly trace: TraceImpl) {
     this.searchOverviewTrack = getOrCreate(
@@ -44,12 +42,12 @@
   }
 
   render(): m.Children {
-    return m('.tickbar');
+    return m('', {style: {height: `${this.height}px`}});
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void {
     ctx.fillStyle = '#999';
-    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+    ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
 
     const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
     ctx.save();
diff --git a/ui/src/frontend/viewer_page/time_axis_panel.ts b/ui/src/frontend/viewer_page/time_axis_panel.ts
index d9d558c..f75738f 100644
--- a/ui/src/frontend/viewer_page/time_axis_panel.ts
+++ b/ui/src/frontend/viewer_page/time_axis_panel.ts
@@ -28,17 +28,15 @@
   MIN_PX_PER_STEP,
   TickType,
 } from './gridline_helper';
-import {Panel} from './panel_container';
 
-export class TimeAxisPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = false;
+export class TimeAxisPanel {
   readonly id = 'time-axis-panel';
+  readonly height = 22;
 
   constructor(private readonly trace: Trace) {}
 
   render(): m.Children {
-    return m('.time-axis-panel');
+    return m('', {style: {height: `${this.height}px`}});
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
@@ -55,7 +53,7 @@
     this.renderPanel(ctx, trackSize);
     ctx.restore();
 
-    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+    ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
   }
 
   private renderOffsetTimestamp(ctx: CanvasRenderingContext2D): void {
diff --git a/ui/src/frontend/viewer_page/time_selection_panel.ts b/ui/src/frontend/viewer_page/time_selection_panel.ts
index 22f9cd1..67e591d 100644
--- a/ui/src/frontend/viewer_page/time_selection_panel.ts
+++ b/ui/src/frontend/viewer_page/time_selection_panel.ts
@@ -28,7 +28,6 @@
   TRACK_SHELL_WIDTH,
 } from '../css_constants';
 import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {Panel} from './panel_container';
 
 export interface BBox {
   x: number;
@@ -133,19 +132,18 @@
   ctx.fillText(label, xPosLabel, yMid);
 }
 
-export class TimeSelectionPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = false;
+export class TimeSelectionPanel {
+  readonly height = 10;
 
   constructor(private readonly trace: TraceImpl) {}
 
   render(): m.Children {
-    return m('.time-selection-panel');
+    return m('', {style: {height: `${this.height}px`}});
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     ctx.fillStyle = '#999';
-    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
+    ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height);
 
     const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
 
@@ -176,11 +174,11 @@
       }
     }
 
-    const localArea = this.trace.timeline.selectedArea;
+    const localSpan = this.trace.timeline.selectedSpan;
     const selection = this.trace.selection.selection;
-    if (localArea !== undefined) {
-      const start = Time.min(localArea.start, localArea.end);
-      const end = Time.max(localArea.start, localArea.end);
+    if (localSpan !== undefined) {
+      const start = Time.min(localSpan.start, localSpan.end);
+      const end = Time.max(localSpan.start, localSpan.end);
       this.renderSpan(ctx, timescale, size, start, end);
     } else {
       if (selection.kind === 'area') {
diff --git a/ui/src/frontend/viewer_page/timeline_header.ts b/ui/src/frontend/viewer_page/timeline_header.ts
new file mode 100644
index 0000000..4496fd2
--- /dev/null
+++ b/ui/src/frontend/viewer_page/timeline_header.ts
@@ -0,0 +1,176 @@
+// 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 {canvasSave} from '../../base/canvas_utils';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D, Size2D} from '../../base/geom';
+import {assertExists} from '../../base/logging';
+import {TimeScale} from '../../base/time_scale';
+import {ZonedInteractionHandler} from '../../base/zoned_interaction_handler';
+import {TraceImpl} from '../../core/trace_impl';
+import {
+  VirtualOverlayCanvas,
+  VirtualOverlayCanvasDrawContext,
+} from '../../components/widgets/virtual_overlay_canvas';
+import {TRACK_SHELL_WIDTH} from '../css_constants';
+import {NotesPanel} from './notes_panel';
+import {TickmarkPanel} from './tickmark_panel';
+import {TimeAxisPanel} from './time_axis_panel';
+import {TimeSelectionPanel} from './time_selection_panel';
+import {
+  shiftDragPanInteraction,
+  wheelNavigationInteraction,
+} from './timeline_interactions';
+
+export interface TimelineHeaderAttrs {
+  // The trace to use for timeline access et al.
+  readonly trace: TraceImpl;
+
+  // Called when the visible area of the timeline changes size. This is the area
+  // to the right of the header is actually rendered on.
+  onTimelineBoundsChange?(rect: Rect2D): void;
+
+  readonly className?: string;
+}
+
+// TODO(stevegolton): The panel concept has been largely removed. It's just
+// defined here so that we don't have to change the implementation of the
+// various header panels listed here. We should consolidate this in the future.
+interface Panel {
+  readonly height: number;
+  render(): m.Children;
+  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D): void;
+}
+
+/**
+ * This component defines the header of the timeline and handles it's mouse
+ * interactions.
+ *
+ * The timeline header contains:
+ * - The axis (ticks) and time labels
+ * - The selection bar
+ * - The notes bar
+ * - The tickmark bar (highlights that appear when searching)
+ */
+export class TimelineHeader implements m.ClassComponent<TimelineHeaderAttrs> {
+  private readonly trash = new DisposableStack();
+  private readonly trace: TraceImpl;
+  private readonly panels: ReadonlyArray<Panel>;
+  private interactions?: ZonedInteractionHandler;
+
+  constructor({attrs}: m.Vnode<TimelineHeaderAttrs>) {
+    this.trace = attrs.trace;
+    this.panels = [
+      new TimeAxisPanel(attrs.trace),
+      new TimeSelectionPanel(attrs.trace),
+      new NotesPanel(attrs.trace),
+      new TickmarkPanel(attrs.trace),
+    ];
+  }
+
+  view({attrs}: m.Vnode<TimelineHeaderAttrs>) {
+    return m(
+      '.pf-timeline-header',
+      {className: attrs.className},
+      m(
+        VirtualOverlayCanvas,
+        {
+          raf: attrs.trace.raf,
+          onCanvasRedraw: (ctx) => {
+            const rect = new Rect2D({
+              left: TRACK_SHELL_WIDTH,
+              right: ctx.virtualCanvasSize.width,
+              top: 0,
+              bottom: 0,
+            });
+            attrs.onTimelineBoundsChange?.(rect);
+            this.drawCanvas(ctx);
+          },
+        },
+        this.panels.map((p) => p.render()),
+      ),
+    );
+  }
+
+  oncreate({dom}: m.VnodeDOM<TimelineHeaderAttrs>) {
+    const timelineHeaderElement = toHTMLElement(dom);
+    this.interactions = new ZonedInteractionHandler(timelineHeaderElement);
+    this.trash.use(this.interactions);
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+
+  private drawCanvas({
+    ctx,
+    virtualCanvasSize,
+  }: VirtualOverlayCanvasDrawContext) {
+    let top = 0;
+    for (const p of this.panels) {
+      using _ = canvasSave(ctx);
+      ctx.translate(0, top);
+      p.renderCanvas(ctx, {width: virtualCanvasSize.width, height: p.height});
+      top += p.height;
+    }
+
+    const timelineRect = new Rect2D({
+      left: TRACK_SHELL_WIDTH,
+      top: 0,
+      right: virtualCanvasSize.width,
+      bottom: virtualCanvasSize.height,
+    });
+
+    // Always grab the latest visible window and create a timescale
+    // out of it.
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, timelineRect);
+
+    assertExists(this.interactions).update([
+      shiftDragPanInteraction(this.trace, timelineRect, timescale),
+      wheelNavigationInteraction(this.trace, timelineRect, timescale),
+      {
+        // Allow making area selections (no tracks) by dragging on the header
+        // timeline.
+        id: 'area-selection',
+        area: timelineRect,
+        drag: {
+          minDistance: 1,
+          cursorWhileDragging: 'text',
+          onDrag: (e) => {
+            const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent);
+            const timeSpan = timescale
+              .pxSpanToHpTimeSpan(dragRect)
+              .toTimeSpan();
+            this.trace.timeline.selectedSpan = timeSpan;
+          },
+          onDragEnd: (e) => {
+            const dragRect = Rect2D.fromPoints(e.dragStart, e.dragCurrent);
+            const timeSpan = timescale
+              .pxSpanToHpTimeSpan(dragRect)
+              .toTimeSpan();
+            this.trace.selection.selectArea({
+              start: timeSpan.start,
+              end: timeSpan.end,
+              trackUris: [],
+            });
+            this.trace.timeline.selectedSpan = undefined;
+          },
+        },
+      },
+    ]);
+  }
+}
diff --git a/ui/src/frontend/viewer_page/timeline_interactions.ts b/ui/src/frontend/viewer_page/timeline_interactions.ts
new file mode 100644
index 0000000..4872d2e
--- /dev/null
+++ b/ui/src/frontend/viewer_page/timeline_interactions.ts
@@ -0,0 +1,75 @@
+// 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.
+
+/**
+ * These interactions may be added to a ZonedInteractionHandler. They define
+ * some common operations that are used by several parts of the timeline such as
+ * shift+drag to pan and mouse wheel navigation.
+ */
+
+import {Rect2D} from '../../base/geom';
+import {TimeScale} from '../../base/time_scale';
+import {Zone} from '../../base/zoned_interaction_handler';
+import {TraceImpl} from '../../core/trace_impl';
+
+const WHEEL_ZOOM_SPEED = -0.02;
+
+export function shiftDragPanInteraction(
+  trace: TraceImpl,
+  rect: Rect2D,
+  timescale: TimeScale,
+): Zone {
+  return {
+    id: 'drag-pan',
+    area: rect,
+    cursor: 'grab',
+    keyModifier: 'shift',
+    drag: {
+      cursorWhileDragging: 'grabbing',
+      onDrag: (e) => {
+        trace.timeline.panVisibleWindow(
+          timescale.pxToDuration(-e.deltaSinceLastEvent.x),
+        );
+      },
+    },
+  };
+}
+
+export function wheelNavigationInteraction(
+  trace: TraceImpl,
+  rect: Rect2D,
+  timescale: TimeScale,
+): Zone {
+  return {
+    id: 'mouse-wheel-navigation',
+    area: rect,
+    onWheel: (e) => {
+      if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
+        const tDelta = timescale.pxToDuration(e.deltaX);
+        trace.timeline.panVisibleWindow(tDelta);
+      } else {
+        if (e.ctrlKey) {
+          const sign = e.deltaY < 0 ? -1 : 1;
+          const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
+          const zoomPx = e.position.x - rect.left;
+          const centerPoint = zoomPx / rect.width;
+          trace.timeline.zoomVisibleWindow(
+            1 - deltaY * WHEEL_ZOOM_SPEED,
+            centerPoint,
+          );
+        }
+      }
+    },
+  };
+}
diff --git a/ui/src/frontend/viewer_page/track_panel.ts b/ui/src/frontend/viewer_page/track_panel.ts
deleted file mode 100644
index bc39777..0000000
--- a/ui/src/frontend/viewer_page/track_panel.ts
+++ /dev/null
@@ -1,471 +0,0 @@
-// Copyright (C) 2018 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 {canvasClip, canvasSave} from '../../base/canvas_utils';
-import {classNames} from '../../base/classnames';
-import {Bounds2D, Size2D, VerticalBounds} from '../../base/geom';
-import {Icons} from '../../base/semantic_icons';
-import {TimeScale} from '../../base/time_scale';
-import {RequiredField} from '../../base/utils';
-import {featureFlags} from '../../core/feature_flags';
-import {raf} from '../../core/raf_scheduler';
-import {TraceImpl} from '../../core/trace_impl';
-import {TrackRenderer} from '../../core/track_manager';
-import {TrackDescriptor, TrackRenderContext} from '../../public/track';
-import {TrackNode} from '../../public/workspace';
-import {Button} from '../../widgets/button';
-import {Intent} from '../../widgets/common';
-import {Popup, PopupPosition} from '../../widgets/popup';
-import {TrackWidget} from '../../widgets/track_widget';
-import {Tree, TreeNode} from '../../widgets/tree';
-import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-import {Panel} from './panel_container';
-import {calculateResolution} from './resolution';
-
-const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
-  id: 'showTrackDetailsButton',
-  name: 'Show track details button',
-  description: 'Show track details button in track shells.',
-  defaultValue: false,
-});
-
-// Default height of a track element that has no track, or is collapsed.
-// Note: This is designed to roughly match the height of a cpu slice track.
-export const DEFAULT_TRACK_HEIGHT_PX = 30;
-
-interface TrackPanelAttrs {
-  readonly trace: TraceImpl;
-  readonly node: TrackNode;
-  readonly indentationLevel: number;
-  readonly trackRenderer?: TrackRenderer;
-  readonly revealOnCreate?: boolean;
-  readonly topOffsetPx: number;
-  readonly reorderable?: boolean;
-}
-
-export class TrackPanel implements Panel {
-  readonly kind = 'panel';
-  readonly selectable = true;
-  readonly trackNode?: TrackNode;
-
-  private readonly attrs: TrackPanelAttrs;
-
-  constructor(attrs: TrackPanelAttrs) {
-    this.attrs = attrs;
-    this.trackNode = attrs.node;
-  }
-
-  get heightPx(): number {
-    const {trackRenderer, node} = this.attrs;
-
-    // If the node is a summary track and is expanded, shrink it to save
-    // vertical real estate).
-    if (node.isSummary && node.expanded) return DEFAULT_TRACK_HEIGHT_PX;
-
-    // Otherwise return the height of the track, if we have one.
-    return trackRenderer?.track.getHeight() ?? DEFAULT_TRACK_HEIGHT_PX;
-  }
-
-  render(): m.Children {
-    const {
-      node,
-      indentationLevel,
-      trackRenderer,
-      revealOnCreate,
-      topOffsetPx,
-      reorderable = false,
-    } = this.attrs;
-
-    const error = trackRenderer?.getError();
-
-    const buttons = [
-      SHOW_TRACK_DETAILS_BUTTON.get() &&
-        renderTrackDetailsButton(node, trackRenderer?.desc),
-      trackRenderer?.track.getTrackShellButtons?.(),
-      node.removable && renderCloseButton(node),
-      // We don't want summary tracks to be pinned as they rarely have
-      // useful information.
-      !node.isSummary && renderPinButton(node),
-      this.renderAreaSelectionCheckbox(node),
-      error && renderCrashButton(error, trackRenderer?.desc.pluginId),
-    ];
-
-    let scrollIntoView = false;
-    const tracks = this.attrs.trace.tracks;
-    if (tracks.scrollToTrackNodeId === node.id) {
-      tracks.scrollToTrackNodeId = undefined;
-      scrollIntoView = true;
-    }
-
-    return m(TrackWidget, {
-      id: node.id,
-      title: node.title,
-      subtitle: trackRenderer?.desc.subtitle,
-      path: node.fullPath.join('/'),
-      heightPx: this.heightPx,
-      error: Boolean(trackRenderer?.getError()),
-      chips: trackRenderer?.desc.chips,
-      indentationLevel,
-      topOffsetPx,
-      buttons,
-      revealOnCreate: revealOnCreate || scrollIntoView,
-      collapsible: node.hasChildren,
-      collapsed: node.collapsed,
-      highlight: this.isHighlighted(node),
-      isSummary: node.isSummary,
-      reorderable,
-      onToggleCollapsed: () => {
-        node.hasChildren && node.toggleCollapsed();
-      },
-      onTrackContentMouseMove: (pos, bounds) => {
-        const timescale = this.getTimescaleForBounds(bounds);
-        trackRenderer?.track.onMouseMove?.({
-          ...pos,
-          timescale,
-        });
-        raf.scheduleCanvasRedraw();
-      },
-      onTrackContentMouseOut: () => {
-        trackRenderer?.track.onMouseOut?.();
-        raf.scheduleCanvasRedraw();
-      },
-      onTrackContentClick: (pos, bounds) => {
-        const timescale = this.getTimescaleForBounds(bounds);
-        raf.scheduleCanvasRedraw();
-        return (
-          trackRenderer?.track.onMouseClick?.({
-            ...pos,
-            timescale,
-          }) ?? false
-        );
-      },
-      onupdate: () => {
-        trackRenderer?.track.onFullRedraw?.();
-      },
-      onMoveBefore: (nodeId: string) => {
-        const targetNode = node.workspace?.getTrackById(nodeId);
-        if (targetNode !== undefined) {
-          // Insert the target node before this one
-          targetNode.parent?.addChildBefore(targetNode, node);
-        }
-      },
-      onMoveAfter: (nodeId: string) => {
-        const targetNode = node.workspace?.getTrackById(nodeId);
-        if (targetNode !== undefined) {
-          // Insert the target node after this one
-          targetNode.parent?.addChildAfter(targetNode, node);
-        }
-      },
-    });
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
-    const {trackRenderer: tr, node} = this.attrs;
-
-    // Don't render if expanded and isSummary
-    if (node.isSummary && node.expanded) {
-      return;
-    }
-
-    const trackSize = {
-      width: size.width - TRACK_SHELL_WIDTH,
-      height: size.height,
-    };
-
-    using _ = canvasSave(ctx);
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
-    canvasClip(ctx, 0, 0, trackSize.width, trackSize.height);
-
-    const visibleWindow = this.attrs.trace.timeline.visibleWindow;
-    const timescale = new TimeScale(visibleWindow, {
-      left: 0,
-      right: trackSize.width,
-    });
-
-    if (tr) {
-      if (!tr.getError()) {
-        const trackRenderCtx: TrackRenderContext = {
-          trackUri: tr.desc.uri,
-          visibleWindow,
-          size: trackSize,
-          resolution: calculateResolution(visibleWindow, trackSize.width),
-          ctx,
-          timescale,
-        };
-        tr.render(trackRenderCtx);
-      }
-    }
-
-    this.highlightIfTrackInAreaSelection(ctx, timescale, node, trackSize);
-  }
-
-  getSliceVerticalBounds(depth: number): VerticalBounds | undefined {
-    if (this.attrs.trackRenderer === undefined) {
-      return undefined;
-    }
-    return this.attrs.trackRenderer.track.getSliceVerticalBounds?.(depth);
-  }
-
-  private getTimescaleForBounds(bounds: Bounds2D) {
-    const timeWindow = this.attrs.trace.timeline.visibleWindow;
-    return new TimeScale(timeWindow, {
-      left: 0,
-      right: bounds.right - bounds.left,
-    });
-  }
-
-  private isHighlighted(node: TrackNode) {
-    // The track should be highlighted if the current search result matches this
-    // track or one of its children.
-    const searchIndex = this.attrs.trace.search.resultIndex;
-    const searchResults = this.attrs.trace.search.searchResults;
-
-    if (searchIndex !== -1 && searchResults !== undefined) {
-      const uri = searchResults.trackUris[searchIndex];
-      // Highlight if this or any children match the search results
-      if (
-        uri === node.uri ||
-        node.flatTracksOrdered.find((t) => t.uri === uri)
-      ) {
-        return true;
-      }
-    }
-
-    const curSelection = this.attrs.trace.selection;
-    if (
-      curSelection.selection.kind === 'track' &&
-      curSelection.selection.trackUri === node.uri
-    ) {
-      return true;
-    }
-
-    return false;
-  }
-
-  private highlightIfTrackInAreaSelection(
-    ctx: CanvasRenderingContext2D,
-    timescale: TimeScale,
-    node: TrackNode,
-    size: Size2D,
-  ) {
-    const selection = this.attrs.trace.selection.selection;
-    if (selection.kind !== 'area') {
-      return;
-    }
-
-    const tracksWithUris = node.flatTracks.filter(
-      (t) => t.uri !== undefined,
-    ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
-
-    let selected = false;
-    if (node.isSummary) {
-      selected = tracksWithUris.some((track) =>
-        selection.trackUris.includes(track.uri),
-      );
-    } else {
-      if (node.uri) {
-        selected = selection.trackUris.includes(node.uri);
-      }
-    }
-
-    if (selected) {
-      const selectedAreaDuration = selection.end - selection.start;
-      ctx.fillStyle = SELECTION_FILL_COLOR;
-      ctx.fillRect(
-        timescale.timeToPx(selection.start),
-        0,
-        timescale.durationToPx(selectedAreaDuration),
-        size.height,
-      );
-    }
-  }
-
-  private renderAreaSelectionCheckbox(node: TrackNode): m.Children {
-    const selectionManager = this.attrs.trace.selection;
-    const selection = selectionManager.selection;
-    if (selection.kind === 'area') {
-      if (node.isSummary) {
-        const tracksWithUris = node.flatTracks.filter(
-          (t) => t.uri !== undefined,
-        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
-        // Check if any nodes within are selected
-        const childTracksInSelection = tracksWithUris.map((t) =>
-          selection.trackUris.includes(t.uri),
-        );
-        if (childTracksInSelection.every((b) => b)) {
-          return m(Button, {
-            onclick: (e: MouseEvent) => {
-              const uris = tracksWithUris.map((t) => t.uri);
-              selectionManager.toggleGroupAreaSelection(uris);
-              e.stopPropagation();
-            },
-            compact: true,
-            icon: Icons.Checkbox,
-            title: 'Remove child tracks from selection',
-          });
-        } else if (childTracksInSelection.some((b) => b)) {
-          return m(Button, {
-            onclick: (e: MouseEvent) => {
-              const uris = tracksWithUris.map((t) => t.uri);
-              selectionManager.toggleGroupAreaSelection(uris);
-              e.stopPropagation();
-            },
-            compact: true,
-            icon: Icons.IndeterminateCheckbox,
-            title: 'Add remaining child tracks to selection',
-          });
-        } else {
-          return m(Button, {
-            onclick: (e: MouseEvent) => {
-              const uris = tracksWithUris.map((t) => t.uri);
-              selectionManager.toggleGroupAreaSelection(uris);
-              e.stopPropagation();
-            },
-            compact: true,
-            icon: Icons.BlankCheckbox,
-            title: 'Add child tracks to selection',
-          });
-        }
-      } else {
-        const nodeUri = node.uri;
-        if (nodeUri) {
-          return (
-            selection.kind === 'area' &&
-            m(Button, {
-              onclick: (e: MouseEvent) => {
-                selectionManager.toggleTrackAreaSelection(nodeUri);
-                e.stopPropagation();
-              },
-              compact: true,
-              ...(selection.trackUris.includes(nodeUri)
-                ? {icon: Icons.Checkbox, title: 'Remove track'}
-                : {icon: Icons.BlankCheckbox, title: 'Add track to selection'}),
-            })
-          );
-        }
-      }
-    }
-    return undefined;
-  }
-}
-
-function renderCrashButton(error: Error, pluginId?: string) {
-  return m(
-    Popup,
-    {
-      trigger: m(Button, {
-        icon: Icons.Crashed,
-        compact: true,
-      }),
-    },
-    m(
-      '.pf-track-crash-popup',
-      m('span', 'This track has crashed.'),
-      pluginId && m('span', `Owning plugin: ${pluginId}`),
-      m(Button, {
-        label: 'View & Report Crash',
-        intent: Intent.Primary,
-        className: Popup.DISMISS_POPUP_GROUP_CLASS,
-        onclick: () => {
-          throw error;
-        },
-      }),
-      // TODO(stevegolton): In the future we should provide a quick way to
-      // disable the plugin, or provide a link to the plugin page, but this
-      // relies on the plugin page being fully functional.
-    ),
-  );
-}
-
-function renderCloseButton(node: TrackNode) {
-  return m(Button, {
-    onclick: (e) => {
-      node.remove();
-      e.stopPropagation();
-    },
-    icon: Icons.Close,
-    title: 'Close track',
-    compact: true,
-  });
-}
-
-function renderPinButton(node: TrackNode): m.Children {
-  const isPinned = node.isPinned;
-  return m(Button, {
-    className: classNames(!isPinned && 'pf-visible-on-hover'),
-    onclick: (e) => {
-      isPinned ? node.unpin() : node.pin();
-      e.stopPropagation();
-    },
-    icon: Icons.Pin,
-    iconFilled: isPinned,
-    title: isPinned ? 'Unpin' : 'Pin to top',
-    compact: true,
-  });
-}
-
-function renderTrackDetailsButton(
-  node: TrackNode,
-  td?: TrackDescriptor,
-): m.Children {
-  let parent = node.parent;
-  let fullPath: m.ChildArray = [node.title];
-  while (parent && parent instanceof TrackNode) {
-    fullPath = [parent.title, ' \u2023 ', ...fullPath];
-    parent = parent.parent;
-  }
-  return m(
-    Popup,
-    {
-      trigger: m(Button, {
-        className: 'pf-visible-on-hover',
-        icon: 'info',
-        title: 'Show track details',
-        compact: true,
-      }),
-      position: PopupPosition.Bottom,
-    },
-    m(
-      '.pf-track-details-dropdown',
-      m(
-        Tree,
-        m(TreeNode, {left: 'Track Node ID', right: node.id}),
-        m(TreeNode, {left: 'Collapsed', right: `${node.collapsed}`}),
-        m(TreeNode, {left: 'URI', right: node.uri}),
-        m(TreeNode, {left: 'Is Summary Track', right: `${node.isSummary}`}),
-        m(TreeNode, {
-          left: 'SortOrder',
-          right: node.sortOrder ?? '0 (undefined)',
-        }),
-        m(TreeNode, {left: 'Path', right: fullPath}),
-        m(TreeNode, {left: 'Title', right: node.title}),
-        m(TreeNode, {
-          left: 'Workspace',
-          right: node.workspace?.title ?? '[no workspace]',
-        }),
-        td && m(TreeNode, {left: 'Plugin ID', right: td.pluginId}),
-        td &&
-          m(
-            TreeNode,
-            {left: 'Tags'},
-            td.tags &&
-              Object.entries(td.tags).map(([key, value]) => {
-                return m(TreeNode, {left: key, right: value?.toString()});
-              }),
-          ),
-      ),
-    ),
-  );
-}
diff --git a/ui/src/frontend/viewer_page/track_tree_view.ts b/ui/src/frontend/viewer_page/track_tree_view.ts
new file mode 100644
index 0000000..2583b09
--- /dev/null
+++ b/ui/src/frontend/viewer_page/track_tree_view.ts
@@ -0,0 +1,725 @@
+// 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.
+
+/**
+ * This module provides the TrackNodeTree mithril component, which is
+ * responsible for rendering out a tree of tracks and drawing their content
+ * onto the canvas.
+ * - Rendering track panels and handling nested and sticky headers.
+ * - Managing the virtual canvas & drawing the grid-lines, tracks and overlays
+ *   onto the canvas.
+ * - Handling track interaction events such as dragging, panning and scrolling.
+ */
+
+import {hex} from 'color-convert';
+import m from 'mithril';
+import {canvasClip, canvasSave} from '../../base/canvas_utils';
+import {classNames} from '../../base/classnames';
+import {DisposableStack} from '../../base/disposable_stack';
+import {findRef, toHTMLElement} from '../../base/dom_utils';
+import {
+  HorizontalBounds,
+  Rect2D,
+  Size2D,
+  VerticalBounds,
+} from '../../base/geom';
+import {HighPrecisionTime} from '../../base/high_precision_time';
+import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
+import {assertExists} from '../../base/logging';
+import {Time} from '../../base/time';
+import {TimeScale} from '../../base/time_scale';
+import {
+  DragEvent,
+  ZonedInteractionHandler,
+} from '../../base/zoned_interaction_handler';
+import {PerfStats, runningStatStr} from '../../core/perf_stats';
+import {TraceImpl} from '../../core/trace_impl';
+import {TrackNode} from '../../public/workspace';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
+import {
+  SELECTION_STROKE_COLOR,
+  TRACK_BORDER_COLOR,
+  TRACK_SHELL_WIDTH,
+} from '../css_constants';
+import {renderFlows} from './flow_events_renderer';
+import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
+import {
+  shiftDragPanInteraction,
+  wheelNavigationInteraction,
+} from './timeline_interactions';
+import {TrackView} from './track_view';
+import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {featureFlags} from '../../core/feature_flags';
+
+const VIRTUAL_TRACK_SCROLLING = featureFlags.register({
+  id: 'virtualTrackScrolling',
+  name: 'Virtual track scrolling',
+  description: `[Experimental] Use virtual scrolling in the timeline view to
+    improve performance on large traces.`,
+  defaultValue: false,
+});
+
+export interface TrackTreeViewAttrs {
+  // Access to the trace, for accessing the track registry / selection manager.
+  readonly trace: TraceImpl;
+
+  // The root track node for tracks to display in this stack. This node is not
+  // actually displayed, only its children are, but it's used for reordering
+  // purposes if `reorderable` is set to true.
+  readonly rootNode: TrackNode;
+
+  // Additional class names to add to the root level element.
+  readonly className?: string;
+
+  // Whether tracks in this stack can be reordered amongst themselves.
+  // Default: false
+  readonly reorderable?: boolean;
+
+  // Scroll to scroll to new tracks as they are added.
+  // Default: false
+  readonly scrollToNewTracks?: boolean;
+}
+
+const TRACK_CONTAINER_REF = 'track-container';
+
+export class TrackTreeView implements m.ClassComponent<TrackTreeViewAttrs> {
+  private readonly trace: TraceImpl;
+  private readonly trash = new DisposableStack();
+  private interactions?: ZonedInteractionHandler;
+  private perfStatsEnabled = false;
+  private trackPerfStats = new WeakMap<TrackNode, PerfStats>();
+  private perfStats = {
+    totalTracks: 0,
+    tracksOnCanvas: 0,
+    renderStats: new PerfStats(10),
+  };
+  private areaDrag?: InProgressAreaSelection;
+  private handleDrag?: InProgressHandleDrag;
+  private canvasRect?: Rect2D;
+
+  constructor({attrs}: m.Vnode<TrackTreeViewAttrs>) {
+    this.trace = attrs.trace;
+  }
+
+  view({attrs}: m.Vnode<TrackTreeViewAttrs>): m.Children {
+    const {trace, scrollToNewTracks, reorderable, className, rootNode} = attrs;
+    const renderedTracks = new Array<TrackView>();
+    let top = 0;
+
+    const renderTrack = (
+      node: TrackNode,
+      depth = 0,
+      stickyTop = 0,
+    ): m.Children => {
+      const trackView = new TrackView(trace, node, top);
+      renderedTracks.push(trackView);
+
+      let childDepth = depth;
+      let childStickyTop = stickyTop;
+      if (!node.headless) {
+        top += trackView.height;
+        ++childDepth;
+        childStickyTop += trackView.height;
+      }
+
+      const children =
+        (node.headless || node.expanded) &&
+        node.hasChildren &&
+        node.children.map((track) =>
+          renderTrack(track, childDepth, childStickyTop),
+        );
+
+      if (node.headless) {
+        return children;
+      } else {
+        const isTrackOnScreen = () => {
+          if (VIRTUAL_TRACK_SCROLLING.get()) {
+            return this.canvasRect?.overlaps({
+              left: 0,
+              right: 1,
+              ...trackView.verticalBounds,
+            });
+          } else {
+            return true;
+          }
+        };
+
+        return trackView.renderDOM(
+          {
+            lite: !Boolean(isTrackOnScreen()),
+            scrollToOnCreate: scrollToNewTracks,
+            reorderable,
+            stickyTop,
+            depth,
+          },
+          children,
+        );
+      }
+    };
+
+    return m(
+      VirtualOverlayCanvas,
+      {
+        raf: attrs.trace.raf,
+        className: classNames(className, 'pf-track-tree'),
+        scrollAxes: 'y',
+        onCanvasRedraw: ({ctx, virtualCanvasSize, canvasRect}) => {
+          this.drawCanvas(
+            ctx,
+            virtualCanvasSize,
+            renderedTracks,
+            canvasRect,
+            rootNode,
+          );
+
+          if (VIRTUAL_TRACK_SCROLLING.get()) {
+            // The VOC can ask us to redraw the canvas for any number of
+            // reasons, we're interested in the case where the canvas rect has
+            // moved (which indicates that the user has scrolled enough to
+            // warrant drawing more content). If so, we should redraw the DOM in
+            // order to keep the track nodes inside the viewport rendering in
+            // full-fat mode.
+            if (
+              this.canvasRect === undefined ||
+              !this.canvasRect.equals(canvasRect)
+            ) {
+              this.canvasRect = canvasRect;
+              m.redraw();
+            }
+          }
+        },
+      },
+      m(
+        '',
+        {ref: TRACK_CONTAINER_REF},
+        rootNode.children.map((track) => renderTrack(track)),
+      ),
+    );
+  }
+
+  oncreate({attrs, dom}: m.VnodeDOM<TrackTreeViewAttrs>) {
+    const interactionTarget = toHTMLElement(
+      assertExists(findRef(dom, TRACK_CONTAINER_REF)),
+    );
+    this.interactions = new ZonedInteractionHandler(interactionTarget);
+    this.trash.use(this.interactions);
+    this.trash.use(
+      attrs.trace.perfDebugging.addContainer({
+        setPerfStatsEnabled: (enable: boolean) => {
+          this.perfStatsEnabled = enable;
+        },
+        renderPerfStats: () => {
+          return [
+            m(
+              '',
+              `${this.perfStats.totalTracks} tracks, ` +
+                `${this.perfStats.tracksOnCanvas} on canvas.`,
+            ),
+            m('', runningStatStr(this.perfStats.renderStats)),
+          ];
+        },
+      }),
+    );
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+
+  private drawCanvas(
+    ctx: CanvasRenderingContext2D,
+    size: Size2D,
+    renderedTracks: ReadonlyArray<TrackView>,
+    floatingCanvasRect: Rect2D,
+    rootNode: TrackNode,
+  ) {
+    const timelineRect = new Rect2D({
+      left: TRACK_SHELL_WIDTH,
+      top: 0,
+      right: size.width,
+      bottom: size.height,
+    });
+
+    // Always grab the latest visible window and create a timescale out of
+    // it.
+    const visibleWindow = this.trace.timeline.visibleWindow;
+    const timescale = new TimeScale(visibleWindow, timelineRect);
+
+    const start = performance.now();
+
+    // Save, translate & clip the canvas to the area of the timeline.
+    using _ = canvasSave(ctx);
+    canvasClip(ctx, timelineRect);
+
+    this.drawGridLines(ctx, timescale, timelineRect);
+
+    const tracksOnCanvas = this.drawTracks(
+      renderedTracks,
+      floatingCanvasRect,
+      size,
+      ctx,
+      timelineRect,
+      visibleWindow,
+    );
+
+    renderFlows(this.trace, ctx, size, renderedTracks, rootNode, timescale);
+    this.drawHoveredNoteVertical(ctx, timescale, size);
+    this.drawHoveredCursorVertical(ctx, timescale, size);
+    this.drawWakeupVertical(ctx, timescale, size);
+    this.drawNoteVerticals(ctx, timescale, size);
+    this.drawAreaSelection(ctx, timescale, size);
+    this.updateInteractions(timelineRect, timescale, size, renderedTracks);
+
+    const renderTime = performance.now() - start;
+    this.updatePerfStats(renderTime, renderedTracks.length, tracksOnCanvas);
+  }
+
+  private drawGridLines(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ): void {
+    ctx.strokeStyle = TRACK_BORDER_COLOR;
+    ctx.lineWidth = 1;
+
+    if (size.width > 0 && timescale.timeSpan.duration > 0n) {
+      const maxMajorTicks = getMaxMajorTicks(size.width);
+      const offset = this.trace.timeline.timestampOffset();
+      for (const {type, time} of generateTicks(
+        timescale.timeSpan.toTimeSpan(),
+        maxMajorTicks,
+        offset,
+      )) {
+        const px = Math.floor(timescale.timeToPx(time));
+        if (type === TickType.MAJOR) {
+          ctx.beginPath();
+          ctx.moveTo(px + 0.5, 0);
+          ctx.lineTo(px + 0.5, size.height);
+          ctx.stroke();
+        }
+      }
+    }
+  }
+
+  private drawTracks(
+    renderedTracks: ReadonlyArray<TrackView>,
+    floatingCanvasRect: Rect2D,
+    size: Size2D,
+    ctx: CanvasRenderingContext2D,
+    timelineRect: Rect2D,
+    visibleWindow: HighPrecisionTimeSpan,
+  ) {
+    let tracksOnCanvas = 0;
+    for (const trackView of renderedTracks) {
+      const {verticalBounds} = trackView;
+      if (
+        floatingCanvasRect.overlaps({
+          ...verticalBounds,
+          left: 0,
+          right: size.width,
+        })
+      ) {
+        trackView.drawCanvas(
+          ctx,
+          timelineRect,
+          visibleWindow,
+          this.perfStatsEnabled,
+          this.trackPerfStats,
+        );
+        ++tracksOnCanvas;
+      }
+    }
+    return tracksOnCanvas;
+  }
+
+  private updateInteractions(
+    timelineRect: Rect2D,
+    timescale: TimeScale,
+    size: Size2D,
+    renderedTracks: ReadonlyArray<TrackView>,
+  ) {
+    const trace = this.trace;
+    const areaSelection =
+      trace.selection.selection.kind === 'area' && trace.selection.selection;
+
+    assertExists(this.interactions).update([
+      shiftDragPanInteraction(trace, timelineRect, timescale),
+      areaSelection !== false && {
+        id: 'start-edit',
+        area: new Rect2D({
+          left: timescale.timeToPx(areaSelection.start) - 5,
+          right: timescale.timeToPx(areaSelection.start) + 5,
+          top: 0,
+          bottom: size.height,
+        }),
+        cursor: 'col-resize',
+        drag: {
+          cursorWhileDragging: 'col-resize',
+          onDrag: (e) => {
+            if (!this.handleDrag) {
+              this.handleDrag = new InProgressHandleDrag(
+                new HighPrecisionTime(areaSelection.end),
+              );
+            }
+            this.handleDrag.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+            trace.timeline.selectedSpan = this.handleDrag
+              .timeSpan()
+              .toTimeSpan();
+          },
+          onDragEnd: (e) => {
+            const newStartTime = timescale
+              .pxToHpTime(e.dragCurrent.x)
+              .toTime('ceil');
+            trace.selection.selectArea({
+              ...areaSelection,
+              end: Time.max(newStartTime, areaSelection.end),
+              start: Time.min(newStartTime, areaSelection.end),
+            });
+            trace.timeline.selectedSpan = undefined;
+            this.handleDrag = undefined;
+          },
+        },
+      },
+      areaSelection !== false && {
+        id: 'end-edit',
+        area: new Rect2D({
+          left: timescale.timeToPx(areaSelection.end) - 5,
+          right: timescale.timeToPx(areaSelection.end) + 5,
+          top: 0,
+          bottom: size.height,
+        }),
+        cursor: 'col-resize',
+        drag: {
+          cursorWhileDragging: 'col-resize',
+          onDrag: (e) => {
+            if (!this.handleDrag) {
+              this.handleDrag = new InProgressHandleDrag(
+                new HighPrecisionTime(areaSelection.start),
+              );
+            }
+            this.handleDrag.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+            trace.timeline.selectedSpan = this.handleDrag
+              .timeSpan()
+              .toTimeSpan();
+          },
+          onDragEnd: (e) => {
+            const newEndTime = timescale
+              .pxToHpTime(e.dragCurrent.x)
+              .toTime('ceil');
+            trace.selection.selectArea({
+              ...areaSelection,
+              end: Time.max(newEndTime, areaSelection.start),
+              start: Time.min(newEndTime, areaSelection.start),
+            });
+            trace.timeline.selectedSpan = undefined;
+            this.handleDrag = undefined;
+          },
+        },
+      },
+      {
+        id: 'area-selection',
+        area: timelineRect,
+        onClick: () => {
+          // If a track hasn't intercepted the click, treat this as a
+          // deselection event.
+          trace.selection.clear();
+        },
+        drag: {
+          minDistance: 1,
+          cursorWhileDragging: 'crosshair',
+          onDrag: (e) => {
+            if (!this.areaDrag) {
+              this.areaDrag = new InProgressAreaSelection(
+                timescale.pxToHpTime(e.dragStart.x),
+                e.dragStart.y,
+              );
+            }
+            this.areaDrag.update(e, timescale);
+            trace.timeline.selectedSpan = this.areaDrag.timeSpan().toTimeSpan();
+          },
+          onDragEnd: (e) => {
+            if (!this.areaDrag) {
+              this.areaDrag = new InProgressAreaSelection(
+                timescale.pxToHpTime(e.dragStart.x),
+                e.dragStart.y,
+              );
+            }
+            this.areaDrag?.update(e, timescale);
+
+            // Find the list of tracks that intersect this selection
+            const trackUris = findTracksInRect(
+              renderedTracks,
+              this.areaDrag.rect(timescale),
+              true,
+            )
+              .map((t) => t.uri)
+              .filter((uri) => uri !== undefined);
+
+            const timeSpan = this.areaDrag.timeSpan().toTimeSpan();
+            trace.selection.selectArea({
+              start: timeSpan.start,
+              end: timeSpan.end,
+              trackUris,
+            });
+
+            trace.timeline.selectedSpan = undefined;
+            this.areaDrag = undefined;
+          },
+        },
+      },
+      wheelNavigationInteraction(trace, timelineRect, timescale),
+    ]);
+  }
+
+  private updatePerfStats(
+    renderTime: number,
+    totalTracks: number,
+    tracksOnCanvas: number,
+  ) {
+    if (!this.perfStatsEnabled) return;
+    this.perfStats.renderStats.addValue(renderTime);
+    this.perfStats.totalTracks = totalTracks;
+    this.perfStats.tracksOnCanvas = tracksOnCanvas;
+  }
+
+  private drawAreaSelection(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    if (this.areaDrag) {
+      ctx.strokeStyle = SELECTION_STROKE_COLOR;
+      ctx.lineWidth = 1;
+      const rect = this.areaDrag.rect(timescale);
+      ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
+    }
+
+    if (this.handleDrag) {
+      const rect = this.handleDrag.hBounds(timescale);
+
+      ctx.strokeStyle = SELECTION_STROKE_COLOR;
+      ctx.lineWidth = 1;
+
+      ctx.beginPath();
+      ctx.moveTo(rect.left, 0);
+      ctx.lineTo(rect.left, size.height);
+      ctx.stroke();
+      ctx.closePath();
+
+      ctx.beginPath();
+      ctx.moveTo(rect.right, 0);
+      ctx.lineTo(rect.right, size.height);
+      ctx.stroke();
+      ctx.closePath();
+    }
+
+    const selection = this.trace.selection.selection;
+    if (selection.kind === 'area') {
+      const startPx = timescale.timeToPx(selection.start);
+      const endPx = timescale.timeToPx(selection.end);
+
+      ctx.strokeStyle = '#8398e6';
+      ctx.lineWidth = 2;
+
+      ctx.beginPath();
+      ctx.moveTo(startPx, 0);
+      ctx.lineTo(startPx, size.height);
+      ctx.stroke();
+      ctx.closePath();
+
+      ctx.beginPath();
+      ctx.moveTo(endPx, 0);
+      ctx.lineTo(endPx, size.height);
+      ctx.stroke();
+      ctx.closePath();
+    }
+  }
+
+  private drawHoveredCursorVertical(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    if (this.trace.timeline.hoverCursorTimestamp !== undefined) {
+      drawVerticalLineAtTime(
+        ctx,
+        timescale,
+        this.trace.timeline.hoverCursorTimestamp,
+        size.height,
+        `#344596`,
+      );
+    }
+  }
+
+  private drawHoveredNoteVertical(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    if (this.trace.timeline.hoveredNoteTimestamp !== undefined) {
+      drawVerticalLineAtTime(
+        ctx,
+        timescale,
+        this.trace.timeline.hoveredNoteTimestamp,
+        size.height,
+        `#aaa`,
+      );
+    }
+  }
+
+  private drawWakeupVertical(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    const selection = this.trace.selection.selection;
+    if (selection.kind === 'track_event' && selection.wakeupTs) {
+      drawVerticalLineAtTime(
+        ctx,
+        timescale,
+        selection.wakeupTs,
+        size.height,
+        `black`,
+      );
+    }
+  }
+
+  private drawNoteVerticals(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    // All marked areas should have semi-transparent vertical lines
+    // marking the start and end.
+    for (const note of this.trace.notes.notes.values()) {
+      if (note.noteType === 'SPAN') {
+        const transparentNoteColor =
+          'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
+        drawVerticalLineAtTime(
+          ctx,
+          timescale,
+          note.start,
+          size.height,
+          transparentNoteColor,
+          1,
+        );
+        drawVerticalLineAtTime(
+          ctx,
+          timescale,
+          note.end,
+          size.height,
+          transparentNoteColor,
+          1,
+        );
+      } else if (note.noteType === 'DEFAULT') {
+        drawVerticalLineAtTime(
+          ctx,
+          timescale,
+          note.timestamp,
+          size.height,
+          note.color,
+        );
+      }
+    }
+  }
+}
+
+/**
+ * Returns a list of track nodes that are contained within a given set of
+ * vertical bounds.
+ *
+ * @param renderedTracks - The list of tracks and their positions.
+ * @param bounds - The bounds in which to check.
+ * @returns - A list of tracks.
+ */
+function findTracksInRect(
+  renderedTracks: ReadonlyArray<TrackView>,
+  bounds: VerticalBounds,
+  recurseCollapsedSummaryTracks = false,
+): TrackNode[] {
+  const tracks: TrackNode[] = [];
+  for (const {node, verticalBounds} of renderedTracks) {
+    const trackRect = new Rect2D({...verticalBounds, left: 0, right: 1});
+    if (trackRect.overlaps({...bounds, left: 0, right: 1})) {
+      // Recurse all child tracks if group node is collapsed and is a summary
+      if (recurseCollapsedSummaryTracks && node.isSummary && node.collapsed) {
+        for (const childTrack of node.flatTracks) {
+          tracks.push(childTrack);
+        }
+      } else {
+        tracks.push(node);
+      }
+    }
+  }
+  return tracks;
+}
+
+// Stores an in-progress area selection.
+class InProgressAreaSelection {
+  currentTime: HighPrecisionTime;
+  currentY: number;
+
+  constructor(
+    readonly startTime: HighPrecisionTime,
+    readonly startY: number,
+  ) {
+    this.currentTime = startTime;
+    this.currentY = startY;
+  }
+
+  update(e: DragEvent, timescale: TimeScale) {
+    this.currentTime = timescale.pxToHpTime(e.dragCurrent.x);
+    this.currentY = e.dragCurrent.y;
+  }
+
+  timeSpan() {
+    return HighPrecisionTimeSpan.fromHpTimes(this.startTime, this.currentTime);
+  }
+
+  rect(timescale: TimeScale) {
+    const horizontal = timescale.hpTimeSpanToPxSpan(this.timeSpan());
+    return Rect2D.fromPoints(
+      {
+        x: horizontal.left,
+        y: this.startY,
+      },
+      {
+        x: horizontal.right,
+        y: this.currentY,
+      },
+    );
+  }
+}
+
+// Stores an in-progress handle drag.
+class InProgressHandleDrag {
+  currentTime: HighPrecisionTime;
+
+  constructor(readonly startTime: HighPrecisionTime) {
+    this.currentTime = startTime;
+  }
+
+  timeSpan() {
+    return HighPrecisionTimeSpan.fromHpTimes(this.startTime, this.currentTime);
+  }
+
+  hBounds(timescale: TimeScale): HorizontalBounds {
+    const horizontal = timescale.hpTimeSpanToPxSpan(this.timeSpan());
+    return new Rect2D({
+      ...horizontal,
+      top: 0,
+      bottom: 0,
+    });
+  }
+}
diff --git a/ui/src/frontend/viewer_page/track_view.ts b/ui/src/frontend/viewer_page/track_view.ts
new file mode 100644
index 0000000..39d4918
--- /dev/null
+++ b/ui/src/frontend/viewer_page/track_view.ts
@@ -0,0 +1,577 @@
+// 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.
+
+/**
+ * This module provides the TrackNodeTree mithril component, which is
+ * responsible for rendering out a tree of tracks and drawing their content
+ * onto the canvas.
+ * - Rendering track panels and handling nested and sticky headers.
+ * - Managing the virtual canvas & drawing the grid-lines, tracks and overlays
+ *   onto the canvas.
+ * - Handling track interaction events such as dragging, panning and scrolling.
+ */
+
+import m from 'mithril';
+import {canvasClip, canvasSave} from '../../base/canvas_utils';
+import {classNames} from '../../base/classnames';
+import {Bounds2D, Rect2D, Size2D, VerticalBounds} from '../../base/geom';
+import {HighPrecisionTimeSpan} from '../../base/high_precision_time_span';
+import {Icons} from '../../base/semantic_icons';
+import {TimeScale} from '../../base/time_scale';
+import {RequiredField} from '../../base/utils';
+import {PerfStats, runningStatStr} from '../../core/perf_stats';
+import {raf} from '../../core/raf_scheduler';
+import {TraceImpl} from '../../core/trace_impl';
+import {TrackRenderer} from '../../core/track_manager';
+import {Track, TrackDescriptor} from '../../public/track';
+import {TrackNode, Workspace} from '../../public/workspace';
+import {Button} from '../../widgets/button';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
+import {TrackShell} from '../../widgets/track_shell';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {SELECTION_FILL_COLOR} from '../css_constants';
+import {calculateResolution} from './resolution';
+
+const TRACK_HEIGHT_MIN_PX = 18;
+const TRACK_HEIGHT_DEFAULT_PX = 30;
+
+function getTrackHeight(node: TrackNode, track?: Track) {
+  // Headless tracks have an effective height of 0.
+  if (node.headless) return 0;
+
+  // Expanded summary tracks don't show any data, so make them a little more
+  // compact to save space.
+  if (node.isSummary && node.expanded) return TRACK_HEIGHT_DEFAULT_PX;
+
+  const trackHeight = track?.getHeight();
+  if (trackHeight === undefined) return TRACK_HEIGHT_DEFAULT_PX;
+
+  // Limit the minimum height of a track, and also round up to the nearest
+  // integer, as sub-integer DOM alignment can cause issues e.g. with sticky
+  // positioning.
+  return Math.ceil(Math.max(trackHeight, TRACK_HEIGHT_MIN_PX));
+}
+
+export interface TrackViewAttrs {
+  // Render a lighter version of this track view (for when tracks are offscreen).
+  readonly lite: boolean;
+  readonly scrollToOnCreate?: boolean;
+  readonly reorderable?: boolean;
+  readonly depth: number;
+  readonly stickyTop: number;
+}
+
+/**
+ * The `TrackView` class is responsible for managing and rendering individual
+ * tracks in the `TrackTreeView` Mithril component. It handles operations such
+ * as:
+ *
+ * - Rendering track content in the DOM and virtual canvas.
+ * - Managing user interactions like dragging, panning, scrolling, and area
+ *   selection.
+ * - Tracking and displaying rendering performance metrics.
+ */
+export class TrackView {
+  readonly node: TrackNode;
+  readonly renderer?: TrackRenderer;
+  readonly height: number;
+  readonly verticalBounds: VerticalBounds;
+
+  private readonly trace: TraceImpl;
+  private readonly descriptor?: TrackDescriptor;
+
+  constructor(trace: TraceImpl, node: TrackNode, top: number) {
+    this.trace = trace;
+    this.node = node;
+
+    if (node.uri) {
+      this.descriptor = trace.tracks.getTrack(node.uri);
+      this.renderer = this.trace.tracks.getTrackRenderer(node.uri);
+    }
+
+    const heightPx = getTrackHeight(node, this.renderer?.track);
+    this.height = heightPx;
+    this.verticalBounds = {top, bottom: top + heightPx};
+  }
+
+  renderDOM(attrs: TrackViewAttrs, children: m.Children) {
+    const {scrollToOnCreate, reorderable = false} = attrs;
+    const {node, renderer, height} = this;
+
+    const buttons = attrs.lite
+      ? []
+      : [
+          renderer?.track.getTrackShellButtons?.(),
+          node.removable && this.renderCloseButton(),
+          // We don't want summary tracks to be pinned as they rarely have
+          // useful information.
+          !node.isSummary && this.renderPinButton(),
+          this.renderTrackMenuButton(),
+          this.renderAreaSelectionCheckbox(),
+        ];
+
+    let scrollIntoView = false;
+    const tracks = this.trace.tracks;
+    if (tracks.scrollToTrackNodeId === node.id) {
+      tracks.scrollToTrackNodeId = undefined;
+      scrollIntoView = true;
+    }
+
+    return m(
+      TrackShell,
+      {
+        id: node.id,
+        title: node.title,
+        subtitle: renderer?.desc.subtitle,
+        ref: node.fullPath.join('/'),
+        heightPx: height,
+        error: renderer?.getError(),
+        chips: renderer?.desc.chips,
+        buttons,
+        scrollToOnCreate: scrollToOnCreate || scrollIntoView,
+        collapsible: node.hasChildren,
+        collapsed: node.collapsed,
+        highlight: this.isHighlighted(),
+        summary: node.isSummary,
+        reorderable,
+        depth: attrs.depth,
+        stickyTop: attrs.stickyTop,
+        pluginId: renderer?.desc.pluginId,
+        lite: attrs.lite,
+        onToggleCollapsed: () => {
+          node.hasChildren && node.toggleCollapsed();
+        },
+        onTrackContentMouseMove: (pos, bounds) => {
+          const timescale = this.getTimescaleForBounds(bounds);
+          renderer?.track.onMouseMove?.({
+            ...pos,
+            timescale,
+          });
+          raf.scheduleCanvasRedraw();
+        },
+        onTrackContentMouseOut: () => {
+          renderer?.track.onMouseOut?.();
+          raf.scheduleCanvasRedraw();
+        },
+        onTrackContentClick: (pos, bounds) => {
+          const timescale = this.getTimescaleForBounds(bounds);
+          raf.scheduleCanvasRedraw();
+          return (
+            renderer?.track.onMouseClick?.({
+              ...pos,
+              timescale,
+            }) ?? false
+          );
+        },
+        onupdate: () => {
+          renderer?.track.onFullRedraw?.();
+        },
+        onMoveBefore: (nodeId: string) => {
+          const targetNode = node.workspace?.getTrackById(nodeId);
+          if (targetNode !== undefined) {
+            // Insert the target node before this one
+            targetNode.parent?.addChildBefore(targetNode, node);
+          }
+        },
+        onMoveAfter: (nodeId: string) => {
+          const targetNode = node.workspace?.getTrackById(nodeId);
+          if (targetNode !== undefined) {
+            // Insert the target node after this one
+            targetNode.parent?.addChildAfter(targetNode, node);
+          }
+        },
+      },
+      children,
+    );
+  }
+
+  drawCanvas(
+    ctx: CanvasRenderingContext2D,
+    rect: Rect2D,
+    visibleWindow: HighPrecisionTimeSpan,
+    perfStatsEnabled: boolean,
+    trackPerfStats: WeakMap<TrackNode, PerfStats>,
+  ) {
+    // For each track we rendered in view(), render it to the canvas. We know the
+    // vertical bounds, so we just need to combine it with the horizontal bounds
+    // and we're golden.
+    const {node, renderer, verticalBounds} = this;
+
+    if (node.isSummary && node.expanded) return;
+    if (renderer?.getError()) return;
+
+    const trackRect = new Rect2D({
+      ...rect,
+      ...verticalBounds,
+    });
+
+    // Track renderers expect to start rendering at (0, 0), so we need to
+    // translate the canvas and create a new timescale.
+    using _ = canvasSave(ctx);
+    canvasClip(ctx, trackRect);
+    ctx.translate(trackRect.left, trackRect.top);
+
+    const timescale = new TimeScale(visibleWindow, {
+      left: 0,
+      right: trackRect.width,
+    });
+
+    const start = performance.now();
+
+    node.uri &&
+      renderer?.render({
+        trackUri: node.uri,
+        visibleWindow,
+        size: trackRect,
+        resolution: calculateResolution(visibleWindow, trackRect.width),
+        ctx,
+        timescale,
+      });
+
+    this.highlightIfTrackInAreaSelection(ctx, timescale, trackRect);
+
+    const renderTime = performance.now() - start;
+
+    if (!perfStatsEnabled) return;
+    this.updateAndRenderTrackPerfStats(
+      ctx,
+      trackRect,
+      renderTime,
+      trackPerfStats,
+    );
+  }
+
+  private renderCloseButton() {
+    return m(Button, {
+      onclick: () => {
+        this.node.remove();
+      },
+      icon: Icons.Close,
+      title: 'Close track',
+      compact: true,
+    });
+  }
+
+  private renderPinButton(): m.Children {
+    const isPinned = this.node.isPinned;
+    return m(Button, {
+      className: classNames(!isPinned && 'pf-visible-on-hover'),
+      onclick: () => {
+        isPinned ? this.node.unpin() : this.node.pin();
+      },
+      icon: Icons.Pin,
+      iconFilled: isPinned,
+      title: isPinned ? 'Unpin' : 'Pin to top',
+      compact: true,
+    });
+  }
+
+  private renderTrackMenuButton(): m.Children {
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Button, {
+          className: 'pf-visible-on-hover',
+          icon: 'more_vert',
+          compact: true,
+          title: 'Track options',
+        }),
+      },
+      m(MenuItem, {
+        label: 'Select track',
+        disabled: !this.node.uri,
+        onclick: () => {
+          this.trace.selection.selectTrack(this.node.uri!);
+        },
+        title: this.node.uri
+          ? 'Select track'
+          : 'Track has no URI and cannot be selected',
+      }),
+      m(MenuItem, {label: 'Track details'}, this.renderTrackDetails()),
+      m(MenuDivider),
+      m(
+        MenuItem,
+        {label: 'Copy to workspace'},
+        this.trace.workspaces.all.map((ws) =>
+          m(MenuItem, {
+            label: ws.title,
+            onclick: () => this.copyToWorkspace(ws),
+          }),
+        ),
+        m(MenuDivider),
+        m(MenuItem, {
+          label: 'New workspace',
+          onclick: () => this.copyToWorkspace(),
+        }),
+      ),
+      m(
+        MenuItem,
+        {label: 'Take to workspace'},
+        this.trace.workspaces.all.map((ws) =>
+          m(MenuItem, {
+            label: ws.title,
+            onclick: async () => {
+              await this.copyToWorkspace(ws);
+              this.trace.workspaces.switchWorkspace(ws);
+            },
+          }),
+        ),
+        m(MenuDivider),
+        m(MenuItem, {
+          label: 'New workspace',
+          onclick: async () => {
+            const ws = await this.copyToWorkspace();
+            ws && this.trace.workspaces.switchWorkspace(ws);
+          },
+        }),
+      ),
+    );
+  }
+
+  private async copyToWorkspace(ws?: Workspace) {
+    if (!ws) {
+      const name = await this.trace.omnibox.prompt(
+        'Enter a name for the new workspace...',
+      );
+      if (!name) return;
+      ws = this.trace.workspaces.createEmptyWorkspace(name);
+    }
+    const newNode = this.node.clone();
+    newNode.removable = true;
+    ws.addChildLast(newNode);
+    return ws;
+  }
+
+  private renderTrackDetails(): m.Children {
+    let parent = this.node.parent;
+    let fullPath: m.ChildArray = [this.node.title];
+    while (parent && parent instanceof TrackNode) {
+      fullPath = [parent.title, ' \u2023 ', ...fullPath];
+      parent = parent.parent;
+    }
+
+    return m(
+      '.pf-track__track-details-popup',
+      m(
+        Tree,
+        m(TreeNode, {left: 'Track Node ID', right: this.node.id}),
+        m(TreeNode, {left: 'Collapsed', right: `${this.node.collapsed}`}),
+        m(TreeNode, {left: 'URI', right: this.node.uri}),
+        m(TreeNode, {
+          left: 'Is Summary Track',
+          right: `${this.node.isSummary}`,
+        }),
+        m(TreeNode, {
+          left: 'SortOrder',
+          right: this.node.sortOrder ?? '0 (undefined)',
+        }),
+        m(TreeNode, {left: 'Path', right: fullPath}),
+        m(TreeNode, {left: 'Title', right: this.node.title}),
+        m(TreeNode, {
+          left: 'Workspace',
+          right: this.node.workspace?.title ?? '[no workspace]',
+        }),
+        this.descriptor &&
+          m(TreeNode, {
+            left: 'Plugin ID',
+            right: this.descriptor.pluginId,
+          }),
+        this.descriptor &&
+          m(
+            TreeNode,
+            {left: 'Tags'},
+            this.descriptor.tags &&
+              Object.entries(this.descriptor.tags).map(([key, value]) => {
+                return m(TreeNode, {left: key, right: value?.toString()});
+              }),
+          ),
+      ),
+    );
+  }
+
+  private getTimescaleForBounds(bounds: Bounds2D) {
+    const timeWindow = this.trace.timeline.visibleWindow;
+    return new TimeScale(timeWindow, {
+      left: 0,
+      right: bounds.right - bounds.left,
+    });
+  }
+
+  private isHighlighted() {
+    const {trace, node} = this;
+    // The track should be highlighted if the current search result matches this
+    // track or one of its children.
+    const searchIndex = trace.search.resultIndex;
+    const searchResults = trace.search.searchResults;
+
+    if (searchIndex !== -1 && searchResults !== undefined) {
+      // using _ = autoTimer();
+      const uri = searchResults.trackUris[searchIndex];
+      // Highlight if this or any children match the search results
+      if (uri === node.uri || node.getTrackByUri(uri)) {
+        return true;
+      }
+    }
+
+    const curSelection = trace.selection;
+    if (
+      curSelection.selection.kind === 'track' &&
+      curSelection.selection.trackUri === node.uri
+    ) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private renderAreaSelectionCheckbox(): m.Children {
+    const {trace, node} = this;
+    const selectionManager = trace.selection;
+    const selection = selectionManager.selection;
+    if (selection.kind === 'area') {
+      if (node.isSummary) {
+        const tracksWithUris = node.flatTracks.filter(
+          (t) => t.uri !== undefined,
+        ) as ReadonlyArray<RequiredField<TrackNode, 'uri'>>;
+
+        // Check if any nodes within are selected
+        const childTracksInSelection = tracksWithUris.map((t) =>
+          selection.trackUris.includes(t.uri),
+        );
+
+        function renderButton(icon: string, title: string) {
+          return m(Button, {
+            onclick: () => {
+              const uris = tracksWithUris.map((t) => t.uri);
+              selectionManager.toggleGroupAreaSelection(uris);
+            },
+            compact: true,
+            icon,
+            title,
+          });
+        }
+
+        if (childTracksInSelection.every((b) => b)) {
+          return renderButton(
+            Icons.Checkbox,
+            'Remove child tracks from selection',
+          );
+        } else if (childTracksInSelection.some((b) => b)) {
+          return renderButton(
+            Icons.IndeterminateCheckbox,
+            'Add remaining child tracks to selection',
+          );
+        } else {
+          return renderButton(
+            Icons.BlankCheckbox,
+            'Add child tracks to selection',
+          );
+        }
+      } else {
+        const nodeUri = node.uri;
+        if (nodeUri) {
+          return (
+            selection.kind === 'area' &&
+            m(Button, {
+              onclick: () => {
+                selectionManager.toggleTrackAreaSelection(nodeUri);
+              },
+              compact: true,
+              ...(selection.trackUris.includes(nodeUri)
+                ? {icon: Icons.Checkbox, title: 'Remove track'}
+                : {icon: Icons.BlankCheckbox, title: 'Add track to selection'}),
+            })
+          );
+        }
+      }
+    }
+    return undefined;
+  }
+
+  private highlightIfTrackInAreaSelection(
+    ctx: CanvasRenderingContext2D,
+    timescale: TimeScale,
+    size: Size2D,
+  ) {
+    const selection = this.trace.selection.selection;
+
+    if (selection.kind !== 'area') {
+      return;
+    }
+
+    let selected = false;
+    if (this.node.isSummary) {
+      // Summary tracks cannot themselves be area-selected. So, as a visual aid,
+      // if this track is a summary track and some of its children are in the
+      // area selection, highlight this track as if it were in the area
+      // selection too.
+      selected = selection.trackUris.some((uri) =>
+        this.node.getTrackByUri(uri),
+      );
+    } else {
+      // For non-summary tracks, simply highlight this track if it's in the area
+      // selection.
+      if (this.node.uri !== undefined) {
+        selected = selection.trackUris.includes(this.node.uri);
+      }
+    }
+
+    if (selected) {
+      const selectedAreaDuration = selection.end - selection.start;
+      ctx.fillStyle = SELECTION_FILL_COLOR;
+      ctx.fillRect(
+        timescale.timeToPx(selection.start),
+        0,
+        timescale.durationToPx(selectedAreaDuration),
+        size.height,
+      );
+    }
+  }
+
+  private updateAndRenderTrackPerfStats(
+    ctx: CanvasRenderingContext2D,
+    size: Size2D,
+    renderTime: number,
+    trackPerfStats: WeakMap<TrackNode, PerfStats>,
+  ) {
+    let renderStats = trackPerfStats.get(this.node);
+    if (renderStats === undefined) {
+      renderStats = new PerfStats();
+      trackPerfStats.set(this.node, renderStats);
+    }
+    renderStats.addValue(renderTime);
+
+    // Draw a green box around the whole track
+    ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)';
+    const lineWidth = 1;
+    ctx.lineWidth = lineWidth;
+    ctx.strokeRect(
+      lineWidth / 2,
+      lineWidth / 2,
+      size.width - lineWidth,
+      size.height - lineWidth,
+    );
+
+    const statW = 300;
+    ctx.font = '10px sans-serif';
+    ctx.textAlign = 'start';
+    ctx.textBaseline = 'alphabetic';
+    ctx.direction = 'inherit';
+    ctx.fillStyle = 'hsl(97, 100%, 96%)';
+    ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
+    ctx.fillStyle = 'hsla(122, 77%, 22%)';
+    const statStr = `Track ${this.node.id} | ` + runningStatStr(renderStats);
+    ctx.fillText(statStr, size.width - statW, size.height - 10);
+  }
+}
diff --git a/ui/src/frontend/viewer_page/viewer_page.ts b/ui/src/frontend/viewer_page/viewer_page.ts
index 45750a5..9a4fc70 100644
--- a/ui/src/frontend/viewer_page/viewer_page.ts
+++ b/ui/src/frontend/viewer_page/viewer_page.ts
@@ -12,39 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {hex} from 'color-convert';
 import m from 'mithril';
-import {removeFalsyValues} from '../../base/array_utils';
-import {canvasClip, canvasSave} from '../../base/canvas_utils';
-import {findRef, toHTMLElement} from '../../base/dom_utils';
-import {Size2D, VerticalBounds} from '../../base/geom';
-import {assertExists} from '../../base/logging';
-import {clamp} from '../../base/math_utils';
-import {Time, TimeSpan} from '../../base/time';
+import {DisposableStack} from '../../base/disposable_stack';
+import {toHTMLElement} from '../../base/dom_utils';
+import {Rect2D} from '../../base/geom';
 import {TimeScale} from '../../base/time_scale';
 import {AppImpl} from '../../core/app_impl';
 import {featureFlags} from '../../core/feature_flags';
 import {PageWithTraceImplAttrs} from '../../core/page_manager';
 import {raf} from '../../core/raf_scheduler';
-import {TraceImpl} from '../../core/trace_impl';
-import {TrackNode} from '../../public/workspace';
-import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from '../css_constants';
-import {renderFlows} from './flow_events_renderer';
-import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper';
-import {NotesPanel} from './notes_panel';
-import {OverviewTimelinePanel} from './overview_timeline_panel';
-import {PanAndZoomHandler} from './pan_and_zoom_handler';
-import {
-  PanelContainer,
-  PanelOrGroup,
-  RenderedPanelInfo,
-} from './panel_container';
+import {OverviewTimeline} from './overview_timeline_panel';
 import {TabPanel} from './tab_panel';
-import {TickmarkPanel} from './tickmark_panel';
-import {TimeAxisPanel} from './time_axis_panel';
-import {TimeSelectionPanel} from './time_selection_panel';
-import {TrackPanel} from './track_panel';
-import {drawVerticalLineAtTime} from './vertical_line_helper';
+import {TimelineHeader} from './timeline_header';
+import {TrackTreeView} from './track_tree_view';
+import {KeyboardNavigationHandler} from './wasd_navigation_handler';
 
 const OVERVIEW_PANEL_FLAG = featureFlags.register({
   id: 'overviewVisible',
@@ -53,613 +34,94 @@
   defaultValue: true,
 });
 
-// Checks if the mousePos is within 3px of the start or end of the
-// current selected time range.
-function onTimeRangeBoundary(
-  trace: TraceImpl,
-  timescale: TimeScale,
-  mousePos: number,
-): 'START' | 'END' | null {
-  const selection = trace.selection.selection;
-  if (selection.kind === 'area') {
-    // If frontend selectedArea exists then we are in the process of editing the
-    // time range and need to use that value instead.
-    const area = trace.timeline.selectedArea
-      ? trace.timeline.selectedArea
-      : selection;
-    const start = timescale.timeToPx(area.start);
-    const end = timescale.timeToPx(area.end);
-    const startDrag = mousePos - TRACK_SHELL_WIDTH;
-    const startDistance = Math.abs(start - startDrag);
-    const endDistance = Math.abs(end - startDrag);
-    const range = 3 * window.devicePixelRatio;
-    // We might be within 3px of both boundaries but we should choose
-    // the closest one.
-    if (startDistance < range && startDistance <= endDistance) return 'START';
-    if (endDistance < range && endDistance <= startDistance) return 'END';
-  }
-  return null;
-}
-
-interface SelectedContainer {
-  readonly containerClass: string;
-  readonly dragStartAbsY: number;
-  readonly dragEndAbsY: number;
-}
-
-/**
- * Top-most level component for the viewer page. Holds tracks, brush timeline,
- * panels, and everything else that's part of the main trace viewer page.
- */
 export class ViewerPage implements m.ClassComponent<PageWithTraceImplAttrs> {
-  private zoomContent?: PanAndZoomHandler;
-  // Used to prevent global deselection if a pan/drag select occurred.
-  private keepCurrentSelection = false;
+  private readonly trash = new DisposableStack();
+  private timelineBounds?: Rect2D;
 
-  private overviewTimelinePanel: OverviewTimelinePanel;
-  private timeAxisPanel: TimeAxisPanel;
-  private timeSelectionPanel: TimeSelectionPanel;
-  private notesPanel: NotesPanel;
-  private tickmarkPanel: TickmarkPanel;
-  private timelineWidthPx?: number;
-  private selectedContainer?: SelectedContainer;
-  private showPanningHint = false;
+  view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
+    const {trace} = attrs;
 
-  private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
-
-  constructor(vnode: m.CVnode<PageWithTraceImplAttrs>) {
-    this.notesPanel = new NotesPanel(vnode.attrs.trace);
-    this.timeAxisPanel = new TimeAxisPanel(vnode.attrs.trace);
-    this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
-    this.tickmarkPanel = new TickmarkPanel(vnode.attrs.trace);
-    this.overviewTimelinePanel = new OverviewTimelinePanel(vnode.attrs.trace);
-    this.notesPanel = new NotesPanel(vnode.attrs.trace);
-    this.timeSelectionPanel = new TimeSelectionPanel(vnode.attrs.trace);
+    return m(
+      '.pf-viewer-page.page',
+      m(
+        TabPanel,
+        {trace},
+        OVERVIEW_PANEL_FLAG.get() &&
+          m(OverviewTimeline, {
+            trace,
+            className: 'pf-viewer-page__overview',
+          }),
+        m(TimelineHeader, {
+          trace,
+          className: 'pf-viewer-page__header',
+          // There are three independent canvases on this page which we could
+          // use keep track of the timeline width, but we use the header one
+          // because it's always rendered.
+          onTimelineBoundsChange: (rect) => (this.timelineBounds = rect),
+        }),
+        // Hide tracks while the trace is loading to prevent thrashing.
+        !AppImpl.instance.isLoadingTrace && [
+          // Don't render pinned tracks if we have none.
+          trace.workspace.pinnedTracks.length > 0 &&
+            m(TrackTreeView, {
+              trace,
+              className: 'pf-viewer-page__pinned-track-tree',
+              rootNode: trace.workspace.pinnedTracksNode,
+              reorderable: true,
+              scrollToNewTracks: true,
+            }),
+          m(TrackTreeView, {
+            trace,
+            className: 'pf-viewer-page__scrolling-track-tree',
+            rootNode: trace.workspace.tracks,
+          }),
+        ],
+      ),
+    );
   }
 
-  oncreate({dom, attrs}: m.CVnodeDOM<PageWithTraceImplAttrs>) {
-    const panZoomElRaw = findRef(dom, this.PAN_ZOOM_CONTENT_REF);
-    const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
+  oncreate(vnode: m.VnodeDOM<PageWithTraceImplAttrs>) {
+    const {attrs, dom} = vnode;
 
-    const {top: panTop} = panZoomEl.getBoundingClientRect();
-    this.zoomContent = new PanAndZoomHandler({
-      element: panZoomEl,
+    // Handles WASD keybindings to pan & zoom
+    const panZoomHandler = new KeyboardNavigationHandler({
+      element: toHTMLElement(dom),
       onPanned: (pannedPx: number) => {
+        if (!this.timelineBounds) return;
         const timeline = attrs.trace.timeline;
-
-        if (this.timelineWidthPx === undefined) return;
-
-        this.keepCurrentSelection = true;
-        const timescale = new TimeScale(timeline.visibleWindow, {
-          left: 0,
-          right: this.timelineWidthPx,
-        });
+        const timescale = new TimeScale(
+          timeline.visibleWindow,
+          this.timelineBounds,
+        );
         const tDelta = timescale.pxToDuration(pannedPx);
         timeline.panVisibleWindow(tDelta);
+        raf.scheduleCanvasRedraw();
       },
       onZoomed: (zoomedPositionPx: number, zoomRatio: number) => {
+        if (!this.timelineBounds) return;
         const timeline = attrs.trace.timeline;
-        // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH.
-        // TODO(hjd): Improve support for zooming in overview timeline.
-        const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH;
-        const rect = dom.getBoundingClientRect();
-        const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
+        const zoomPx = zoomedPositionPx - this.timelineBounds.left;
+        const centerPoint = zoomPx / this.timelineBounds.width;
         timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
         raf.scheduleCanvasRedraw();
       },
-      editSelection: (currentPx: number) => {
-        if (this.timelineWidthPx === undefined) return false;
-        const timescale = new TimeScale(attrs.trace.timeline.visibleWindow, {
-          left: 0,
-          right: this.timelineWidthPx,
-        });
-        return onTimeRangeBoundary(attrs.trace, timescale, currentPx) !== null;
-      },
-      onSelection: (
-        dragStartX: number,
-        dragStartY: number,
-        prevX: number,
-        currentX: number,
-        currentY: number,
-        editing: boolean,
-      ) => {
-        const traceTime = attrs.trace.traceInfo;
-        const timeline = attrs.trace.timeline;
-
-        if (this.timelineWidthPx === undefined) return;
-
-        // TODO(stevegolton): Don't get the windowSpan from globals, get it from
-        // here!
-        const {visibleWindow} = timeline;
-        const timespan = visibleWindow.toTimeSpan();
-        this.keepCurrentSelection = true;
-
-        const timescale = new TimeScale(timeline.visibleWindow, {
-          left: 0,
-          right: this.timelineWidthPx,
-        });
-
-        if (editing) {
-          const selection = attrs.trace.selection.selection;
-          if (selection.kind === 'area') {
-            const area = attrs.trace.timeline.selectedArea
-              ? attrs.trace.timeline.selectedArea
-              : selection;
-            let newTime = timescale
-              .pxToHpTime(currentX - TRACK_SHELL_WIDTH)
-              .toTime();
-            // Have to check again for when one boundary crosses over the other.
-            const curBoundary = onTimeRangeBoundary(
-              attrs.trace,
-              timescale,
-              prevX,
-            );
-            if (curBoundary == null) return;
-            const keepTime = curBoundary === 'START' ? area.end : area.start;
-            // Don't drag selection outside of current screen.
-            if (newTime < keepTime) {
-              newTime = Time.max(newTime, timespan.start);
-            } else {
-              newTime = Time.min(newTime, timespan.end);
-            }
-            // When editing the time range we always use the saved tracks,
-            // since these will not change.
-            timeline.selectArea(
-              Time.max(Time.min(keepTime, newTime), traceTime.start),
-              Time.min(Time.max(keepTime, newTime), traceTime.end),
-              selection.trackUris,
-            );
-          }
-        } else {
-          let startPx = Math.min(dragStartX, currentX) - TRACK_SHELL_WIDTH;
-          let endPx = Math.max(dragStartX, currentX) - TRACK_SHELL_WIDTH;
-          if (startPx < 0 && endPx < 0) return;
-          startPx = clamp(startPx, 0, this.timelineWidthPx);
-          endPx = clamp(endPx, 0, this.timelineWidthPx);
-          timeline.selectArea(
-            timescale.pxToHpTime(startPx).toTime('floor'),
-            timescale.pxToHpTime(endPx).toTime('ceil'),
-          );
-
-          const absStartY = dragStartY + panTop;
-          const absCurrentY = currentY + panTop;
-          if (this.selectedContainer === undefined) {
-            for (const c of dom.querySelectorAll('.pf-panel-container')) {
-              const {top, bottom} = c.getBoundingClientRect();
-              if (top <= absStartY && absCurrentY <= bottom) {
-                const stack = assertExists(c.querySelector('.pf-panel-stack'));
-                const stackTop = stack.getBoundingClientRect().top;
-                this.selectedContainer = {
-                  containerClass: Array.from(c.classList).filter(
-                    (x) => x !== 'pf-panel-container',
-                  )[0],
-                  dragStartAbsY: -stackTop + absStartY,
-                  dragEndAbsY: -stackTop + absCurrentY,
-                };
-                break;
-              }
-            }
-          } else {
-            const c = assertExists(
-              dom.querySelector(`.${this.selectedContainer.containerClass}`),
-            );
-            const {top, bottom} = c.getBoundingClientRect();
-            const boundedCurrentY = Math.min(
-              Math.max(top, absCurrentY),
-              bottom,
-            );
-            const stack = assertExists(c.querySelector('.pf-panel-stack'));
-            const stackTop = stack.getBoundingClientRect().top;
-            this.selectedContainer = {
-              ...this.selectedContainer,
-              dragEndAbsY: -stackTop + boundedCurrentY,
-            };
-          }
-          this.showPanningHint = true;
-        }
-        raf.scheduleCanvasRedraw();
-      },
-      endSelection: (edit: boolean) => {
-        this.selectedContainer = undefined;
-        const area = attrs.trace.timeline.selectedArea;
-        // If we are editing we need to pass the current id through to ensure
-        // the marked area with that id is also updated.
-        if (edit) {
-          const selection = attrs.trace.selection.selection;
-          if (selection.kind === 'area' && area) {
-            attrs.trace.selection.selectArea({...area});
-          }
-        } else if (area) {
-          attrs.trace.selection.selectArea({...area});
-        }
-        // Now the selection has ended we stored the final selected area in the
-        // global state and can remove the in progress selection from the
-        // timeline.
-        attrs.trace.timeline.deselectArea();
-        // Full redraw to color track shell.
-        raf.scheduleFullRedraw();
-      },
     });
+    this.trash.use(panZoomHandler);
+    this.onupdate(vnode);
+  }
+
+  onupdate({attrs}: m.VnodeDOM<PageWithTraceImplAttrs>) {
+    // TODO(stevegolton): It's assumed that the TrackStacks will call into
+    // trace.tracks.getTrackRenderer() in their view() functions which will mark
+    // track renderers as used. We call flushOldTracks() here as it's guaranteed
+    // to be called after view() on all child elements, and is only called once
+    // per render cycle. However, this approach involves a bit too much magic.
+    // The TODO is to sort this out and make it so the track flushing is
+    // consolidated into one place.
+    attrs.trace.tracks.flushOldTracks();
   }
 
   onremove() {
-    if (this.zoomContent) this.zoomContent[Symbol.dispose]();
-  }
-
-  view({attrs}: m.CVnode<PageWithTraceImplAttrs>) {
-    const scrollingPanels = renderToplevelPanels(attrs.trace);
-
-    const result = m(
-      '.page.viewer-page',
-      m(
-        TabPanel,
-        {
-          trace: attrs.trace,
-        },
-        m(
-          '.pan-and-zoom-content',
-          {
-            ref: this.PAN_ZOOM_CONTENT_REF,
-            onclick: () => {
-              // We don't want to deselect when panning/drag selecting.
-              if (this.keepCurrentSelection) {
-                this.keepCurrentSelection = false;
-                return;
-              }
-              attrs.trace.selection.clear();
-            },
-          },
-          m(
-            '.pf-timeline-header',
-            m(PanelContainer, {
-              trace: attrs.trace,
-              className: 'header-panel-container',
-              panels: removeFalsyValues([
-                OVERVIEW_PANEL_FLAG.get() && this.overviewTimelinePanel,
-                this.timeAxisPanel,
-                this.timeSelectionPanel,
-                this.notesPanel,
-                this.tickmarkPanel,
-              ]),
-              selectedYRange: this.getYRange('header-panel-container'),
-            }),
-            m('.scrollbar-spacer-vertical'),
-          ),
-          m(PanelContainer, {
-            trace: attrs.trace,
-            className: 'pinned-panel-container',
-            panels: AppImpl.instance.isLoadingTrace
-              ? []
-              : attrs.trace.workspace.pinnedTracks.map((trackNode) => {
-                  if (trackNode.uri) {
-                    const tr = attrs.trace.tracks.getTrackRenderer(
-                      trackNode.uri,
-                    );
-                    return new TrackPanel({
-                      trace: attrs.trace,
-                      reorderable: true,
-                      node: trackNode,
-                      trackRenderer: tr,
-                      revealOnCreate: true,
-                      indentationLevel: 0,
-                      topOffsetPx: 0,
-                    });
-                  } else {
-                    return new TrackPanel({
-                      trace: attrs.trace,
-                      node: trackNode,
-                      revealOnCreate: true,
-                      indentationLevel: 0,
-                      topOffsetPx: 0,
-                    });
-                  }
-                }),
-            renderUnderlay: (ctx, size) =>
-              renderUnderlay(attrs.trace, ctx, size),
-            renderOverlay: (ctx, size, panels) =>
-              renderOverlay(
-                attrs.trace,
-                ctx,
-                size,
-                panels,
-                attrs.trace.workspace.pinnedTracksNode,
-              ),
-            selectedYRange: this.getYRange('pinned-panel-container'),
-          }),
-          m(PanelContainer, {
-            trace: attrs.trace,
-            className: 'scrolling-panel-container',
-            panels: AppImpl.instance.isLoadingTrace ? [] : scrollingPanels,
-            onPanelStackResize: (width) => {
-              const timelineWidth = width - TRACK_SHELL_WIDTH;
-              this.timelineWidthPx = timelineWidth;
-            },
-            renderUnderlay: (ctx, size) =>
-              renderUnderlay(attrs.trace, ctx, size),
-            renderOverlay: (ctx, size, panels) =>
-              renderOverlay(
-                attrs.trace,
-                ctx,
-                size,
-                panels,
-                attrs.trace.workspace.tracks,
-              ),
-            selectedYRange: this.getYRange('scrolling-panel-container'),
-          }),
-        ),
-      ),
-
-      this.showPanningHint && m(HelpPanningNotification),
-    );
-
-    attrs.trace.tracks.flushOldTracks();
-    return result;
-  }
-
-  private getYRange(cls: string): VerticalBounds | undefined {
-    if (this.selectedContainer?.containerClass !== cls) {
-      return undefined;
-    }
-    const {dragStartAbsY, dragEndAbsY} = this.selectedContainer;
-    return {
-      top: Math.min(dragStartAbsY, dragEndAbsY),
-      bottom: Math.max(dragStartAbsY, dragEndAbsY),
-    };
-  }
-}
-
-function renderUnderlay(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  canvasSize: Size2D,
-): void {
-  const size = {
-    width: canvasSize.width - TRACK_SHELL_WIDTH,
-    height: canvasSize.height,
-  };
-
-  using _ = canvasSave(ctx);
-  ctx.translate(TRACK_SHELL_WIDTH, 0);
-
-  const timewindow = trace.timeline.visibleWindow;
-  const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
-
-  // Just render the gridlines - these should appear underneath all tracks
-  drawGridLines(trace, ctx, timewindow.toTimeSpan(), timescale, size);
-}
-
-function renderOverlay(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  canvasSize: Size2D,
-  panels: ReadonlyArray<RenderedPanelInfo>,
-  trackContainer: TrackNode,
-): void {
-  const size = {
-    width: canvasSize.width - TRACK_SHELL_WIDTH,
-    height: canvasSize.height,
-  };
-
-  using _ = canvasSave(ctx);
-  ctx.translate(TRACK_SHELL_WIDTH, 0);
-  canvasClip(ctx, 0, 0, size.width, size.height);
-
-  // TODO(primiano): plumb the TraceImpl obj throughout the viwer page.
-  renderFlows(trace, ctx, size, panels, trackContainer);
-
-  const timewindow = trace.timeline.visibleWindow;
-  const timescale = new TimeScale(timewindow, {left: 0, right: size.width});
-
-  renderHoveredNoteVertical(trace, ctx, timescale, size);
-  renderHoveredCursorVertical(trace, ctx, timescale, size);
-  renderWakeupVertical(trace, ctx, timescale, size);
-  renderNoteVerticals(trace, ctx, timescale, size);
-}
-
-// Render the toplevel "scrolling" tracks and track groups
-function renderToplevelPanels(trace: TraceImpl): PanelOrGroup[] {
-  return renderNodes(trace, trace.workspace.children, 0, 0);
-}
-
-// Given a list of tracks and a filter term, return a list pf panels filtered by
-// the filter term
-function renderNodes(
-  trace: TraceImpl,
-  nodes: ReadonlyArray<TrackNode>,
-  indent: number,
-  topOffsetPx: number,
-): PanelOrGroup[] {
-  return nodes.flatMap((node) => {
-    if (node.headless) {
-      // Render children as if this node doesn't exist
-      return renderNodes(trace, node.children, indent, topOffsetPx);
-    } else if (node.children.length === 0) {
-      return renderTrackPanel(trace, node, indent, topOffsetPx);
-    } else {
-      const headerPanel = renderTrackPanel(trace, node, indent, topOffsetPx);
-      const isSticky = node.isSummary;
-      const nextTopOffsetPx = isSticky
-        ? topOffsetPx + headerPanel.heightPx
-        : topOffsetPx;
-      return {
-        kind: 'group',
-        collapsed: node.collapsed,
-        header: headerPanel,
-        sticky: isSticky, // && node.collapsed??
-        topOffsetPx,
-        childPanels: node.collapsed
-          ? []
-          : renderNodes(trace, node.children, indent + 1, nextTopOffsetPx),
-      };
-    }
-  });
-}
-
-function renderTrackPanel(
-  trace: TraceImpl,
-  trackNode: TrackNode,
-  indent: number,
-  topOffsetPx: number,
-) {
-  let tr = undefined;
-  if (trackNode.uri) {
-    tr = trace.tracks.getTrackRenderer(trackNode.uri);
-  }
-  return new TrackPanel({
-    trace,
-    node: trackNode,
-    trackRenderer: tr,
-    indentationLevel: indent,
-    topOffsetPx,
-  });
-}
-
-export function drawGridLines(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  timespan: TimeSpan,
-  timescale: TimeScale,
-  size: Size2D,
-): void {
-  ctx.strokeStyle = TRACK_BORDER_COLOR;
-  ctx.lineWidth = 1;
-
-  if (size.width > 0 && timespan.duration > 0n) {
-    const maxMajorTicks = getMaxMajorTicks(size.width);
-    const offset = trace.timeline.timestampOffset();
-    for (const {type, time} of generateTicks(timespan, maxMajorTicks, offset)) {
-      const px = Math.floor(timescale.timeToPx(time));
-      if (type === TickType.MAJOR) {
-        ctx.beginPath();
-        ctx.moveTo(px + 0.5, 0);
-        ctx.lineTo(px + 0.5, size.height);
-        ctx.stroke();
-      }
-    }
-  }
-}
-
-export function renderHoveredCursorVertical(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size2D,
-) {
-  if (trace.timeline.hoverCursorTimestamp !== undefined) {
-    drawVerticalLineAtTime(
-      ctx,
-      timescale,
-      trace.timeline.hoverCursorTimestamp,
-      size.height,
-      `#344596`,
-    );
-  }
-}
-
-export function renderHoveredNoteVertical(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size2D,
-) {
-  if (trace.timeline.hoveredNoteTimestamp !== undefined) {
-    drawVerticalLineAtTime(
-      ctx,
-      timescale,
-      trace.timeline.hoveredNoteTimestamp,
-      size.height,
-      `#aaa`,
-    );
-  }
-}
-
-export function renderWakeupVertical(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size2D,
-) {
-  const selection = trace.selection.selection;
-  if (selection.kind === 'track_event' && selection.wakeupTs) {
-    drawVerticalLineAtTime(
-      ctx,
-      timescale,
-      selection.wakeupTs,
-      size.height,
-      `black`,
-    );
-  }
-}
-
-export function renderNoteVerticals(
-  trace: TraceImpl,
-  ctx: CanvasRenderingContext2D,
-  timescale: TimeScale,
-  size: Size2D,
-) {
-  // All marked areas should have semi-transparent vertical lines
-  // marking the start and end.
-  for (const note of trace.notes.notes.values()) {
-    if (note.noteType === 'SPAN') {
-      const transparentNoteColor =
-        'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
-      drawVerticalLineAtTime(
-        ctx,
-        timescale,
-        note.start,
-        size.height,
-        transparentNoteColor,
-        1,
-      );
-      drawVerticalLineAtTime(
-        ctx,
-        timescale,
-        note.end,
-        size.height,
-        transparentNoteColor,
-        1,
-      );
-    } else if (note.noteType === 'DEFAULT') {
-      drawVerticalLineAtTime(
-        ctx,
-        timescale,
-        note.timestamp,
-        size.height,
-        note.color,
-      );
-    }
-  }
-}
-
-class HelpPanningNotification implements m.ClassComponent {
-  private readonly PANNING_HINT_KEY = 'dismissedPanningHint';
-  private dismissed = localStorage.getItem(this.PANNING_HINT_KEY) === 'true';
-
-  view() {
-    // Do not show the help notification in embedded mode because local storage
-    // does not persist for iFrames. The host is responsible for communicating
-    // to users that they can press '?' for help.
-    if (AppImpl.instance.embeddedMode || this.dismissed) {
-      return;
-    }
-    return m(
-      '.helpful-hint',
-      m(
-        '.hint-text',
-        'Are you trying to pan? Use the WASD keys or hold shift to click ' +
-          "and drag. Press '?' for more help.",
-      ),
-      m(
-        'button.hint-dismiss-button',
-        {
-          onclick: () => {
-            this.dismissed = true;
-            localStorage.setItem(this.PANNING_HINT_KEY, 'true');
-            raf.scheduleFullRedraw();
-          },
-        },
-        'Dismiss',
-      ),
-    );
+    this.trash.dispose();
   }
 }
diff --git a/ui/src/frontend/viewer_page/pan_and_zoom_handler.ts b/ui/src/frontend/viewer_page/wasd_navigation_handler.ts
similarity index 65%
rename from ui/src/frontend/viewer_page/pan_and_zoom_handler.ts
rename to ui/src/frontend/viewer_page/wasd_navigation_handler.ts
index 758e124..b43074c 100644
--- a/ui/src/frontend/viewer_page/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/viewer_page/wasd_navigation_handler.ts
@@ -14,8 +14,6 @@
 
 import {DisposableStack} from '../../base/disposable_stack';
 import {currentTargetOffset, elementIsEditable} from '../../base/dom_utils';
-import {DragGestureHandler} from '../../base/drag_gesture_handler';
-import {raf} from '../../core/raf_scheduler';
 import {Animation} from '../animation';
 
 // When first starting to pan or zoom, move at least this many units.
@@ -39,14 +37,6 @@
 const ZOOM_RATIO_PER_FRAME = 0.008;
 const KEYBOARD_PAN_PX_PER_FRAME = 8;
 
-// Scroll wheel animation steps.
-const HORIZONTAL_WHEEL_PAN_SPEED = 1;
-const WHEEL_ZOOM_SPEED = -0.02;
-
-const EDITING_RANGE_CURSOR = 'ew-resize';
-const DRAG_CURSOR = 'default';
-const PAN_CURSOR = 'move';
-
 // Use key mapping based on the 'KeyboardEvent.code' property vs the
 // 'KeyboardEvent.key', because the former corresponds to the physical key
 // position rather than the glyph printed on top of it, and is unaffected by
@@ -88,15 +78,13 @@
 }
 
 /**
- * Enables horizontal pan and zoom with mouse-based drag and WASD navigation.
+ * Enables horizontal pan and zoom with WASD navigation.
  */
-export class PanAndZoomHandler implements Disposable {
+export class KeyboardNavigationHandler implements Disposable {
   private mousePositionX: number | null = null;
   private boundOnMouseMove = this.onMouseMove.bind(this);
-  private boundOnWheel = this.onWheel.bind(this);
   private boundOnKeyDown = this.onKeyDown.bind(this);
   private boundOnKeyUp = this.onKeyUp.bind(this);
-  private shiftDown = false;
   private panning: Pan = Pan.None;
   private panOffsetPx = 0;
   private targetPanOffsetPx = 0;
@@ -109,96 +97,30 @@
   private element: HTMLElement;
   private onPanned: (movedPx: number) => void;
   private onZoomed: (zoomPositionPx: number, zoomRatio: number) => void;
-  private editSelection: (currentPx: number) => boolean;
-  private onSelection: (
-    dragStartX: number,
-    dragStartY: number,
-    prevX: number,
-    currentX: number,
-    currentY: number,
-    editing: boolean,
-  ) => void;
-  private endSelection: (edit: boolean) => void;
   private trash: DisposableStack;
 
   constructor({
     element,
     onPanned,
     onZoomed,
-    editSelection,
-    onSelection,
-    endSelection,
   }: {
     element: HTMLElement;
     onPanned: (movedPx: number) => void;
     onZoomed: (zoomPositionPx: number, zoomRatio: number) => void;
-    editSelection: (currentPx: number) => boolean;
-    onSelection: (
-      dragStartX: number,
-      dragStartY: number,
-      prevX: number,
-      currentX: number,
-      currentY: number,
-      editing: boolean,
-    ) => void;
-    endSelection: (edit: boolean) => void;
   }) {
     this.element = element;
     this.onPanned = onPanned;
     this.onZoomed = onZoomed;
-    this.editSelection = editSelection;
-    this.onSelection = onSelection;
-    this.endSelection = endSelection;
     this.trash = new DisposableStack();
 
     document.body.addEventListener('keydown', this.boundOnKeyDown);
     document.body.addEventListener('keyup', this.boundOnKeyUp);
     this.element.addEventListener('mousemove', this.boundOnMouseMove);
-    this.element.addEventListener('wheel', this.boundOnWheel, {passive: true});
     this.trash.defer(() => {
-      this.element.removeEventListener('wheel', this.boundOnWheel);
       this.element.removeEventListener('mousemove', this.boundOnMouseMove);
       document.body.removeEventListener('keyup', this.boundOnKeyUp);
       document.body.removeEventListener('keydown', this.boundOnKeyDown);
     });
-
-    let prevX = -1;
-    let dragStartX = -1;
-    let dragStartY = -1;
-    let edit = false;
-    this.trash.use(
-      new DragGestureHandler(
-        this.element,
-        /* onDrag */ (x, y) => {
-          if (this.shiftDown) {
-            this.onPanned(prevX - x);
-          } else {
-            this.onSelection(dragStartX, dragStartY, prevX, x, y, edit);
-          }
-          prevX = x;
-        },
-        /* onDragStarted */ (x, y) => {
-          prevX = x;
-          dragStartX = x;
-          dragStartY = y;
-          edit = this.editSelection(x);
-          // Set the cursor style based on where the cursor is when the drag
-          // starts.
-          if (edit) {
-            this.element.style.cursor = EDITING_RANGE_CURSOR;
-          } else if (!this.shiftDown) {
-            this.element.style.cursor = DRAG_CURSOR;
-          }
-        },
-        /* onDragFinished */ () => {
-          // Reset the cursor now the drag has ended.
-          this.element.style.cursor = this.shiftDown ? PAN_CURSOR : DRAG_CURSOR;
-          dragStartX = -1;
-          dragStartY = -1;
-          this.endSelection(edit);
-        },
-      ),
-    );
   }
 
   [Symbol.dispose]() {
@@ -242,30 +164,6 @@
 
   private onMouseMove(e: MouseEvent) {
     this.mousePositionX = currentTargetOffset(e).x;
-
-    // Only change the cursor when hovering, the DragGestureHandler handles
-    // changing the cursor during drag events. This avoids the problem of
-    // the cursor flickering between styles if you drag fast and get too
-    // far from the current time range.
-    if (e.buttons === 0) {
-      if (this.editSelection(this.mousePositionX)) {
-        this.element.style.cursor = EDITING_RANGE_CURSOR;
-      } else {
-        this.element.style.cursor = this.shiftDown ? PAN_CURSOR : DRAG_CURSOR;
-      }
-    }
-  }
-
-  private onWheel(e: WheelEvent) {
-    if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
-      this.onPanned(e.deltaX * HORIZONTAL_WHEEL_PAN_SPEED);
-      raf.scheduleCanvasRedraw();
-    } else if (e.ctrlKey && this.mousePositionX !== null) {
-      const sign = e.deltaY < 0 ? -1 : 1;
-      const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
-      this.onZoomed(this.mousePositionX, deltaY * WHEEL_ZOOM_SPEED);
-      raf.scheduleCanvasRedraw();
-    }
   }
 
   // Due to a bug in chrome, we get onKeyDown events fired where the payload is
@@ -276,8 +174,6 @@
     if (e instanceof KeyboardEvent) {
       if (elementIsEditable(e.target)) return;
 
-      this.updateShift(e.shiftKey);
-
       if (e.ctrlKey || e.metaKey) return;
 
       if (keyToPan(e) !== Pan.None) {
@@ -304,8 +200,6 @@
 
   private onKeyUp(e: Event) {
     if (e instanceof KeyboardEvent) {
-      this.updateShift(e.shiftKey);
-
       if (e.ctrlKey || e.metaKey) return;
 
       if (keyToPan(e) === this.panning) {
@@ -316,15 +210,4 @@
       }
     }
   }
-
-  // TODO(hjd): Move this shift handling into the viewer page.
-  private updateShift(down: boolean) {
-    if (down === this.shiftDown) return;
-    this.shiftDown = down;
-    if (this.shiftDown) {
-      this.element.style.cursor = PAN_CURSOR;
-    } else if (this.mousePositionX !== null) {
-      this.element.style.cursor = DRAG_CURSOR;
-    }
-  }
 }
diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts
index 180a81c..737bed3 100644
--- a/ui/src/plugins/com.android.InputEvents/index.ts
+++ b/ui/src/plugins/com.android.InputEvents/index.ts
@@ -17,10 +17,11 @@
 import {Trace} from '../../public/trace';
 import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
 import {TrackNode} from '../../public/workspace';
-import {getOrCreateUserInteractionGroup} from '../../public/standard_groups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'com.android.InputEvents';
+  static readonly dependencies = [StandardGroupsPlugin];
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     const cnt = await ctx.engine.query(`
@@ -58,7 +59,9 @@
       track,
     });
     const node = new TrackNode({uri, title});
-    const group = getOrCreateUserInteractionGroup(ctx.workspace);
+    const group = ctx.plugins
+      .getPlugin(StandardGroupsPlugin)
+      .getOrCreateStandardGroup(ctx.workspace, 'USER_INTERACTION');
     group.addChildInOrder(node);
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
index d81ca13..b6b4b91 100644
--- a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
@@ -18,9 +18,14 @@
 } from '../../components/tracks/query_counter_track';
 import {PerfettoPlugin} from '../../public/plugin';
 import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
 import {TrackNode} from '../../public/workspace';
-import {NUM_NULL} from '../../trace_processor/query_result';
+import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
 import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
 
 async function registerAllocsTrack(
   ctx: Trace,
@@ -41,7 +46,11 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.AndroidDmabuf';
-  static readonly dependencies = [ProcessThreadGroupsPlugin];
+  static readonly dependencies = [
+    ProcessThreadGroupsPlugin,
+    StandardGroupsPlugin,
+    TraceProcessorTrackPlugin,
+  ];
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     const e = ctx.engine;
@@ -82,5 +91,73 @@
           ?.addChildInOrder(new TrackNode({uri, title: 'dmabuf allocs'}));
       }
     }
+    const memoryGroupFn = () => {
+      return ctx.plugins
+        .getPlugin(StandardGroupsPlugin)
+        .getOrCreateStandardGroup(ctx.workspace, 'MEMORY');
+    };
+    const node = await addGlobalCounter(ctx, memoryGroupFn);
+    await addGlobalAllocs(ctx, () => {
+      return node ?? memoryGroupFn();
+    });
   }
 }
+
+async function addGlobalCounter(ctx: Trace, parent: () => TrackNode) {
+  const track = await ctx.engine.query(`
+    select id, name
+    from track
+    where type = 'android_dma_heap'
+  `);
+  const it = track.maybeFirstRow({id: NUM, name: STR});
+  if (!it) {
+    return undefined;
+  }
+  const {id, name: title} = it;
+  const uri = `/android_dmabuf_counter`;
+  ctx.tracks.registerTrack({
+    uri,
+    title,
+    tags: {
+      kind: COUNTER_TRACK_KIND,
+      trackIds: [id],
+    },
+    track: new TraceProcessorCounterTrack(ctx, uri, {}, id, title),
+  });
+  const node = new TrackNode({
+    uri,
+    title,
+  });
+  parent().addChildInOrder(node);
+  return node;
+}
+
+async function addGlobalAllocs(ctx: Trace, parent: () => TrackNode) {
+  const track = await ctx.engine.query(`
+    select name, group_concat(id) as trackIds
+    from track
+    where type = 'android_dma_allocations'
+    group by name
+  `);
+  const it = track.maybeFirstRow({trackIds: STR, name: STR});
+  if (!it) {
+    return undefined;
+  }
+  const {trackIds, name: title} = it;
+  const uri = `/android_dmabuf_allocs`;
+  const ids = trackIds.split(',').map((x) => Number(x));
+  ctx.tracks.registerTrack({
+    uri,
+    title,
+    tags: {
+      kind: SLICE_TRACK_KIND,
+      trackIds: ids,
+    },
+    track: new TraceProcessorSliceTrack(ctx, uri, undefined, ids),
+  });
+  const node = new TrackNode({
+    uri,
+    title,
+  });
+  parent().addChildInOrder(node);
+}
diff --git a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
index 63d888a..1cdba09 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
@@ -144,8 +144,6 @@
         visibleSpan,
         this.pagination,
       );
-
-      attrs.trace.scheduleFullRedraw();
     });
   }
 
@@ -221,7 +219,6 @@
         onchange: (e: Event) => {
           const selectionValue = (e.target as HTMLSelectElement).value;
           attrs.onSelect(Number(selectionValue));
-          attrs.trace.scheduleFullRedraw();
         },
       },
       optionComponents,
@@ -243,7 +240,6 @@
         // updated with the latest key (onkeyup).
         const htmlElement = e.target as HTMLInputElement;
         attrs.onChange(htmlElement.value);
-        attrs.trace.scheduleFullRedraw();
       },
     });
   }
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
deleted file mode 100644
index e1c94d1..0000000
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
+++ /dev/null
@@ -1,451 +0,0 @@
-// Copyright (C) 2021 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 {removeFalsyValues} from '../../base/array_utils';
-import {TrackNode} from '../../public/workspace';
-import {SLICE_TRACK_KIND} from '../../public/track_kinds';
-import {Trace} from '../../public/trace';
-import {PerfettoPlugin} from '../../public/plugin';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-import {AsyncSliceTrack} from './async_slice_track';
-import {exists} from '../../base/utils';
-import {assertExists, assertTrue} from '../../base/logging';
-import {SliceSelectionAggregator} from './slice_selection_aggregator';
-import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
-
-export default class implements PerfettoPlugin {
-  static readonly id = 'dev.perfetto.AsyncSlices';
-  static readonly dependencies = [ProcessThreadGroupsPlugin];
-
-  async onTraceLoad(ctx: Trace): Promise<void> {
-    const trackIdsToUris = new Map<number, string>();
-
-    await this.addGlobalAsyncTracks(ctx, trackIdsToUris);
-    await this.addProcessAsyncSliceTracks(ctx, trackIdsToUris);
-    await this.addThreadAsyncSliceTracks(ctx, trackIdsToUris);
-
-    ctx.selection.registerSqlSelectionResolver({
-      sqlTableName: 'slice',
-      callback: async (id: number) => {
-        // Locate the track for a given id in the slice table
-        const result = await ctx.engine.query(`
-          select
-            track_id as trackId
-          from
-            slice
-          where slice.id = ${id}
-        `);
-
-        if (result.numRows() === 0) {
-          return undefined;
-        }
-
-        const {trackId} = result.firstRow({
-          trackId: NUM,
-        });
-
-        const trackUri = trackIdsToUris.get(trackId);
-        if (!trackUri) {
-          return undefined;
-        }
-
-        return {
-          trackUri,
-          eventId: id,
-        };
-      },
-    });
-
-    ctx.selection.registerAreaSelectionAggregator(
-      new SliceSelectionAggregator(),
-    );
-  }
-
-  async addGlobalAsyncTracks(
-    ctx: Trace,
-    trackIdsToUris: Map<number, string>,
-  ): Promise<void> {
-    const {engine} = ctx;
-    const rawGlobalAsyncTracks = await engine.query(`
-      include perfetto module graphs.search;
-      include perfetto module viz.summary.tracks;
-
-      with global_tracks_grouped as (
-        select
-          t.parent_id,
-          t.name,
-          group_concat(t.id) as trackIds,
-          count() as trackCount,
-          ifnull(min(a.order_id), 0) as order_id
-        from track t
-        join _slice_track_summary s using (id)
-        left join _track_event_tracks_ordered a USING (id)
-        where
-          s.is_legacy_global
-          and (name != 'Suspend/Resume Latency' or name is null)
-        group by parent_id, name
-        order by parent_id, order_id
-      ),
-      intermediate_groups as (
-        select
-          t.name,
-          t.id,
-          t.parent_id,
-          ifnull(a.order_id, 0) as order_id
-        from graph_reachable_dfs!(
-          (
-            select id as source_node_id, parent_id as dest_node_id
-            from track
-            where parent_id is not null
-          ),
-          (
-            select distinct parent_id as node_id
-            from global_tracks_grouped
-            where parent_id is not null
-          )
-        ) g
-        join track t on g.node_id = t.id
-        left join _track_event_tracks_ordered a USING (id)
-      )
-      select
-        t.name as name,
-        t.parent_id as parentId,
-        t.trackIds as trackIds,
-        t.order_id as orderId,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks_grouped t
-      union all
-      select
-        t.name as name,
-        t.parent_id as parentId,
-        cast_string!(t.id) as trackIds,
-        t.order_id as orderId,
-        NULL as maxDepth
-      from intermediate_groups t
-      left join _slice_track_summary s using (id)
-      where s.id is null
-      order by parentId, orderId
-    `);
-    const it = rawGlobalAsyncTracks.iter({
-      name: STR_NULL,
-      parentId: NUM_NULL,
-      trackIds: STR,
-      orderId: NUM,
-      maxDepth: NUM_NULL,
-    });
-
-    // Create a map of track nodes by id
-    const trackMap = new Map<
-      number,
-      {parentId: number | null; trackNode: TrackNode}
-    >();
-
-    for (; it.valid(); it.next()) {
-      const rawName = it.name === null ? undefined : it.name;
-      const title = getTrackName({
-        name: rawName,
-        kind: SLICE_TRACK_KIND,
-      });
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const maxDepth = it.maxDepth;
-
-      if (maxDepth === null) {
-        assertTrue(trackIds.length == 1);
-        const trackNode = new TrackNode({title, sortOrder: -25});
-        trackMap.set(trackIds[0], {parentId: it.parentId, trackNode});
-      } else {
-        const uri = `/async_slices_${rawName}_${it.parentId}`;
-        ctx.tracks.registerTrack({
-          uri,
-          title,
-          tags: {
-            trackIds,
-            kind: SLICE_TRACK_KIND,
-            scope: 'global',
-          },
-          track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
-        });
-        const trackNode = new TrackNode({
-          uri,
-          title,
-          sortOrder: it.orderId,
-        });
-        trackIds.forEach((id) => {
-          trackMap.set(id, {parentId: it.parentId, trackNode});
-          trackIdsToUris.set(id, uri);
-        });
-      }
-    }
-
-    // Attach track nodes to parents / or the workspace if they have no parent
-    trackMap.forEach(({parentId, trackNode}) => {
-      if (exists(parentId)) {
-        const parent = assertExists(trackMap.get(parentId));
-        parent.trackNode.addChildInOrder(trackNode);
-      } else {
-        ctx.workspace.addChildInOrder(trackNode);
-      }
-    });
-  }
-
-  async addCpuTracks(
-    ctx: Trace,
-    trackIdsToUris: Map<number, string>,
-  ): Promise<void> {
-    const {engine} = ctx;
-    const res = await engine.query(`
-      include perfetto module viz.summary.tracks;
-
-      with global_tracks_grouped as (
-        select
-          t.name,
-          group_concat(t.id) as trackIds,
-          count() as trackCount
-        from cpu_track t
-        join _slice_track_summary using (id)
-        group by name
-      )
-      select
-        t.name as name,
-        t.trackIds as trackIds,
-        __max_layout_depth(t.trackCount, t.trackIds) as maxDepth
-      from global_tracks_grouped t
-    `);
-    const it = res.iter({
-      name: STR_NULL,
-      trackIds: STR,
-      maxDepth: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const rawName = it.name === null ? undefined : it.name;
-      const title = getTrackName({
-        name: rawName,
-        kind: SLICE_TRACK_KIND,
-      });
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const maxDepth = it.maxDepth;
-
-      const uri = `/cpu_slices_${rawName}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title,
-        tags: {
-          trackIds,
-          kind: SLICE_TRACK_KIND,
-          scope: 'global',
-        },
-        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
-      });
-      const trackNode = new TrackNode({
-        uri,
-        title,
-      });
-      ctx.workspace.addChildInOrder(trackNode);
-      trackIds.forEach((id) => {
-        trackIdsToUris.set(id, uri);
-      });
-    }
-  }
-
-  async addProcessAsyncSliceTracks(
-    ctx: Trace,
-    trackIdsToUris: Map<number, string>,
-  ): Promise<void> {
-    const result = await ctx.engine.query(`
-      select
-        upid,
-        t.name as trackName,
-        t.track_ids as trackIds,
-        process.name as processName,
-        process.pid as pid,
-        t.parent_id as parentId,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth
-      from _process_track_summary_by_upid_and_parent_id_and_name t
-      join process using (upid)
-      where t.name is null or t.name not glob "* Timeline"
-    `);
-
-    const it = result.iter({
-      upid: NUM,
-      parentId: NUM_NULL,
-      trackName: STR_NULL,
-      trackIds: STR,
-      processName: STR_NULL,
-      pid: NUM_NULL,
-      maxDepth: NUM,
-    });
-
-    const trackMap = new Map<
-      number,
-      {parentId: number | null; upid: number; trackNode: TrackNode}
-    >();
-
-    for (; it.valid(); it.next()) {
-      const upid = it.upid;
-      const trackName = it.trackName;
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const processName = it.processName;
-      const pid = it.pid;
-      const maxDepth = it.maxDepth;
-
-      const kind = SLICE_TRACK_KIND;
-      const title = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind,
-      });
-
-      const uri = `/process_${upid}/async_slices_${rawTrackIds}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title,
-        tags: {
-          trackIds,
-          kind: SLICE_TRACK_KIND,
-          scope: 'process',
-          upid,
-        },
-        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
-      });
-      const track = new TrackNode({uri, title, sortOrder: 30});
-      trackIds.forEach((id) => {
-        trackMap.set(id, {trackNode: track, parentId: it.parentId, upid});
-        trackIdsToUris.set(id, uri);
-      });
-    }
-
-    // Attach track nodes to parents / or the workspace if they have no parent
-    trackMap.forEach((t) => {
-      const parent = exists(t.parentId) && trackMap.get(t.parentId);
-      if (parent !== false && parent !== undefined) {
-        parent.trackNode.addChildInOrder(t.trackNode);
-      } else {
-        const processGroup = ctx.plugins
-          .getPlugin(ProcessThreadGroupsPlugin)
-          .getGroupForProcess(t.upid);
-        processGroup?.addChildInOrder(t.trackNode);
-      }
-    });
-  }
-
-  async addThreadAsyncSliceTracks(
-    ctx: Trace,
-    trackIdsToUris: Map<number, string>,
-  ): Promise<void> {
-    const result = await ctx.engine.query(`
-      include perfetto module viz.summary.slices;
-      include perfetto module viz.summary.threads;
-      include perfetto module viz.threads;
-
-      select
-        t.utid,
-        t.parent_id as parentId,
-        thread.upid,
-        t.name as trackName,
-        thread.name as threadName,
-        thread.tid as tid,
-        t.track_ids as trackIds,
-        __max_layout_depth(t.track_count, t.track_ids) as maxDepth,
-        k.is_main_thread as isMainThread,
-        k.is_kernel_thread AS isKernelThread
-      from _thread_track_summary_by_utid_and_name t
-      join _threads_with_kernel_flag k using(utid)
-      join thread using (utid)
-    `);
-
-    const it = result.iter({
-      utid: NUM,
-      parentId: NUM_NULL,
-      upid: NUM_NULL,
-      trackName: STR_NULL,
-      trackIds: STR,
-      maxDepth: NUM,
-      isMainThread: NUM_NULL,
-      isKernelThread: NUM,
-      threadName: STR_NULL,
-      tid: NUM_NULL,
-    });
-
-    const trackMap = new Map<
-      number,
-      {parentId: number | null; utid: number; trackNode: TrackNode}
-    >();
-
-    for (; it.valid(); it.next()) {
-      const {
-        utid,
-        parentId,
-        upid,
-        trackName,
-        isMainThread,
-        isKernelThread,
-        maxDepth,
-        threadName,
-        tid,
-      } = it;
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const title = getTrackName({
-        name: trackName,
-        utid,
-        tid,
-        threadName,
-        kind: 'Slices',
-      });
-
-      const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title,
-        tags: {
-          trackIds,
-          kind: SLICE_TRACK_KIND,
-          scope: 'thread',
-          utid,
-          upid: upid ?? undefined,
-          ...(isKernelThread === 1 && {kernelThread: true}),
-        },
-        chips: removeFalsyValues([
-          isKernelThread === 0 && isMainThread === 1 && 'main thread',
-        ]),
-        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
-      });
-      const track = new TrackNode({uri, title, sortOrder: 20});
-      trackIds.forEach((id) => {
-        trackMap.set(id, {trackNode: track, parentId, utid});
-        trackIdsToUris.set(id, uri);
-      });
-    }
-
-    // Attach track nodes to parents / or the workspace if they have no parent
-    trackMap.forEach((t) => {
-      const parent = exists(t.parentId) && trackMap.get(t.parentId);
-      if (parent !== false && parent !== undefined) {
-        parent.trackNode.addChildInOrder(t.trackNode);
-      } else {
-        const group = ctx.plugins
-          .getPlugin(ProcessThreadGroupsPlugin)
-          .getGroupForThread(t.utid);
-        group?.addChildInOrder(t.trackNode);
-      }
-    });
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.Counter/index.ts b/ui/src/plugins/dev.perfetto.Counter/index.ts
deleted file mode 100644
index 879b139..0000000
--- a/ui/src/plugins/dev.perfetto.Counter/index.ts
+++ /dev/null
@@ -1,417 +0,0 @@
-// Copyright (C) 2021 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 {
-  NUM_NULL,
-  STR_NULL,
-  LONG_NULL,
-  NUM,
-  STR,
-} from '../../trace_processor/query_result';
-import {Trace} from '../../public/trace';
-import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
-import {PerfettoPlugin} from '../../public/plugin';
-import {getThreadUriPrefix, getTrackName} from '../../public/utils';
-import {CounterOptions} from '../../components/tracks/base_counter_track';
-import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
-import {exists} from '../../base/utils';
-import {TrackNode} from '../../public/workspace';
-import {CounterSelectionAggregator} from './counter_selection_aggregator';
-import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
-
-const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
-const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
-
-type Modes = CounterOptions['yMode'];
-
-// Sets the default 'mode' for counter tracks. If the regex matches
-// then the paired mode is used. Entries are in priority order so the
-// first match wins.
-const COUNTER_REGEX: [RegExp, Modes][] = [
-  // Power counters make more sense in rate mode since you're typically
-  // interested in the slope of the graph rather than the absolute
-  // value.
-  [new RegExp('^power..*$'), 'rate'],
-  // Same for cumulative PSI stall time counters, e.g., psi.cpu.some.
-  [new RegExp('^psi..*$'), 'rate'],
-  // Same for network counters.
-  [NETWORK_TRACK_REGEX, 'rate'],
-  // Entity residency
-  [ENTITY_RESIDENCY_REGEX, 'rate'],
-];
-
-function getCounterMode(name: string): Modes | undefined {
-  for (const [re, mode] of COUNTER_REGEX) {
-    if (name.match(re)) {
-      return mode;
-    }
-  }
-  return undefined;
-}
-
-function getDefaultCounterOptions(name: string): Partial<CounterOptions> {
-  const options: Partial<CounterOptions> = {};
-  options.yMode = getCounterMode(name);
-
-  if (name.endsWith('_pct')) {
-    options.yOverrideMinimum = 0;
-    options.yOverrideMaximum = 100;
-    options.unit = '%';
-  }
-
-  if (name.startsWith('power.')) {
-    options.yRangeSharingKey = 'power';
-  }
-
-  // TODO(stevegolton): We need to rethink how this works for virtual memory.
-  // The problem is we can easily have > 10GB virtual memory which dwarfs
-  // physical memory making other memory tracks difficult to read.
-
-  // if (name.startsWith('mem.')) {
-  //   options.yRangeSharingKey = 'mem';
-  // }
-
-  // All 'Entity residency: foo bar1234' tracks should share a y-axis
-  // with 'Entity residency: foo baz5678' etc tracks:
-  {
-    const r = new RegExp('Entity residency: ([^ ]+) ');
-    const m = r.exec(name);
-    if (m) {
-      options.yRangeSharingKey = `entity-residency-${m[1]}`;
-    }
-  }
-
-  {
-    const r = new RegExp('GPU .* Frequency');
-    const m = r.exec(name);
-    if (m) {
-      options.yRangeSharingKey = 'gpu-frequency';
-    }
-  }
-
-  return options;
-}
-
-export default class implements PerfettoPlugin {
-  static readonly id = 'dev.perfetto.Counter';
-  static readonly dependencies = [ProcessThreadGroupsPlugin];
-
-  async onTraceLoad(ctx: Trace): Promise<void> {
-    await this.addCounterTracks(ctx);
-    await this.addGpuFrequencyTracks(ctx);
-    await this.addCpuFreqLimitCounterTracks(ctx);
-    await this.addCpuTimeCounterTracks(ctx);
-    await this.addCpuPerfCounterTracks(ctx);
-    await this.addThreadCounterTracks(ctx);
-    await this.addProcessCounterTracks(ctx);
-
-    ctx.selection.registerAreaSelectionAggregator(
-      new CounterSelectionAggregator(),
-    );
-  }
-
-  private async addCounterTracks(ctx: Trace) {
-    const result = await ctx.engine.query(`
-      select name, id, unit
-      from (
-        select name, id, unit
-        from counter_track
-        join _counter_track_summary using (id)
-        where is_legacy_global
-        union
-        select name, id, unit
-        from gpu_counter_track
-        join _counter_track_summary using (id)
-        where name != 'gpufreq'
-      )
-      order by name
-    `);
-
-    // Add global or GPU counter tracks that are not bound to any pid/tid.
-    const it = result.iter({
-      name: STR,
-      unit: STR_NULL,
-      id: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const trackId = it.id;
-      const title = it.name;
-      const unit = it.unit ?? undefined;
-
-      const uri = `/counter_${trackId}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          trackIds: [trackId],
-        },
-        track: new TraceProcessorCounterTrack(
-          ctx,
-          uri,
-          {
-            ...getDefaultCounterOptions(title),
-            unit,
-          },
-          trackId,
-          title,
-        ),
-      });
-      const track = new TrackNode({uri, title});
-      ctx.workspace.addChildInOrder(track);
-    }
-  }
-
-  async addCpuFreqLimitCounterTracks(ctx: Trace): Promise<void> {
-    const cpuFreqLimitCounterTracksSql = `
-      select name, id
-      from cpu_counter_track
-      join _counter_track_summary using (id)
-      where name glob "Cpu * Freq Limit"
-      order by name asc
-    `;
-
-    this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql, 'cpuFreqLimit');
-  }
-
-  async addCpuTimeCounterTracks(ctx: Trace): Promise<void> {
-    const cpuTimeCounterTracksSql = `
-      select name, id
-      from cpu_counter_track
-      join _counter_track_summary using (id)
-      where name glob "cpu.times.*"
-      order by name asc
-    `;
-    this.addCpuCounterTracks(ctx, cpuTimeCounterTracksSql, 'cpuTime');
-  }
-
-  async addCpuPerfCounterTracks(ctx: Trace): Promise<void> {
-    // Perf counter tracks are bound to CPUs, follow the scheduling and
-    // frequency track naming convention ("Cpu N ...").
-    // Note: we might not have a track for a given cpu if no data was seen from
-    // it. This might look surprising in the UI, but placeholder tracks are
-    // wasteful as there's no way of collapsing global counter tracks at the
-    // moment.
-    const addCpuPerfCounterTracksSql = `
-      select printf("Cpu %u %s", cpu, name) as name, id
-      from perf_counter_track as pct
-      join _counter_track_summary using (id)
-      order by perf_session_id asc, pct.name asc, cpu asc
-    `;
-    this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql, 'cpuPerf');
-  }
-
-  async addCpuCounterTracks(
-    ctx: Trace,
-    sql: string,
-    scope: string,
-  ): Promise<void> {
-    const result = await ctx.engine.query(sql);
-
-    const it = result.iter({
-      name: STR,
-      id: NUM,
-    });
-
-    for (; it.valid(); it.next()) {
-      const name = it.name;
-      const trackId = it.id;
-      const uri = `counter.cpu.${trackId}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title: name,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          trackIds: [trackId],
-          scope,
-        },
-        track: new TraceProcessorCounterTrack(
-          ctx,
-          uri,
-          getDefaultCounterOptions(name),
-          trackId,
-          name,
-        ),
-      });
-      const trackNode = new TrackNode({uri, title: name, sortOrder: -20});
-      ctx.workspace.addChildInOrder(trackNode);
-    }
-  }
-
-  async addThreadCounterTracks(ctx: Trace): Promise<void> {
-    const result = await ctx.engine.query(`
-      select
-        thread_counter_track.name as trackName,
-        utid,
-        upid,
-        tid,
-        thread.name as threadName,
-        thread_counter_track.id as trackId,
-        thread.start_ts as startTs,
-        thread.end_ts as endTs
-      from thread_counter_track
-      join _counter_track_summary using (id)
-      join thread using(utid)
-      where thread_counter_track.name != 'thread_time'
-    `);
-
-    const it = result.iter({
-      startTs: LONG_NULL,
-      trackId: NUM,
-      endTs: LONG_NULL,
-      trackName: STR_NULL,
-      utid: NUM,
-      upid: NUM_NULL,
-      tid: NUM_NULL,
-      threadName: STR_NULL,
-    });
-    for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const upid = it.upid;
-      const tid = it.tid;
-      const trackId = it.trackId;
-      const trackName = it.trackName;
-      const threadName = it.threadName;
-      const kind = COUNTER_TRACK_KIND;
-      const name = getTrackName({
-        name: trackName,
-        utid,
-        tid,
-        kind,
-        threadName,
-        threadTrack: true,
-      });
-      const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title: name,
-        tags: {
-          kind,
-          trackIds: [trackId],
-          utid,
-          upid: upid ?? undefined,
-          scope: 'thread',
-        },
-        track: new TraceProcessorCounterTrack(
-          ctx,
-          uri,
-          getDefaultCounterOptions(name),
-          trackId,
-          name,
-        ),
-      });
-      const group = ctx.plugins
-        .getPlugin(ProcessThreadGroupsPlugin)
-        .getGroupForThread(utid);
-      const track = new TrackNode({uri, title: name, sortOrder: 30});
-      group?.addChildInOrder(track);
-    }
-  }
-
-  async addProcessCounterTracks(ctx: Trace): Promise<void> {
-    const result = await ctx.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 _counter_track_summary using (id)
-      join process using(upid)
-      order by trackName;
-    `);
-    const it = result.iter({
-      trackId: NUM,
-      trackName: STR_NULL,
-      upid: NUM,
-      pid: NUM_NULL,
-      processName: STR_NULL,
-    });
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      const trackId = it.trackId;
-      const pid = it.pid;
-      const trackName = it.trackName;
-      const upid = it.upid;
-      const processName = it.processName;
-      const kind = COUNTER_TRACK_KIND;
-      const name = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        kind,
-        processName,
-        ...(exists(trackName) && {trackName}),
-      });
-      const uri = `/process_${upid}/counter_${trackId}`;
-      ctx.tracks.registerTrack({
-        uri,
-        title: name,
-        tags: {
-          kind,
-          trackIds: [trackId],
-          upid,
-          scope: 'process',
-        },
-        track: new TraceProcessorCounterTrack(
-          ctx,
-          uri,
-          getDefaultCounterOptions(name),
-          trackId,
-          name,
-        ),
-      });
-      const group = ctx.plugins
-        .getPlugin(ProcessThreadGroupsPlugin)
-        .getGroupForProcess(upid);
-      const track = new TrackNode({uri, title: name, sortOrder: 20});
-      group?.addChildInOrder(track);
-    }
-  }
-
-  private async addGpuFrequencyTracks(ctx: Trace) {
-    const engine = ctx.engine;
-
-    const result = await engine.query(`
-      select id, gpu_id as gpuId
-      from gpu_counter_track
-      join _counter_track_summary using (id)
-      where name = 'gpufreq'
-    `);
-    const it = result.iter({id: NUM, gpuId: NUM});
-    for (; it.valid(); it.next()) {
-      const uri = `/gpu_frequency_${it.gpuId}`;
-      const name = `Gpu ${it.gpuId} Frequency`;
-      ctx.tracks.registerTrack({
-        uri,
-        title: name,
-        tags: {
-          kind: COUNTER_TRACK_KIND,
-          trackIds: [it.id],
-          scope: 'gpuFreq',
-        },
-        track: new TraceProcessorCounterTrack(
-          ctx,
-          uri,
-          getDefaultCounterOptions(name),
-          it.id,
-          name,
-        ),
-      });
-      const track = new TrackNode({uri, title: name, sortOrder: -20});
-      ctx.workspace.addChildInOrder(track);
-    }
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
index 4880341..9811843 100644
--- a/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts
@@ -26,7 +26,11 @@
 import {uuidv4Sql} from '../../base/uuid';
 import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
 import {Point2D} from '../../base/geom';
-import {createView, createVirtualTable} from '../../trace_processor/sql_utils';
+import {
+  createPerfettoTable,
+  createView,
+  createVirtualTable,
+} from '../../trace_processor/sql_utils';
 import {AsyncDisposableStack} from '../../base/disposable_stack';
 import {Trace} from '../../public/trace';
 
@@ -68,6 +72,9 @@
 
   async onCreate() {
     this.trash = new AsyncDisposableStack();
+    await this.trace.engine.query(`
+      INCLUDE PERFETTO MODULE counters.intervals;
+    `);
     if (this.config.idleTrackId === undefined) {
       this.trash.use(
         await createView(
@@ -75,26 +82,32 @@
           `raw_freq_idle_${this.trackUuid}`,
           `
             select ts, dur, value as freqValue, -1 as idleValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.freqTrackId}
+            from counter_leading_intervals!((
+              select id, ts, track_id, value
+              from counter
+              where track_id = ${this.config.freqTrackId}
+            ))
           `,
         ),
       );
     } else {
       this.trash.use(
-        await createView(
+        await createPerfettoTable(
           this.trace.engine,
           `raw_freq_${this.trackUuid}`,
           `
             select ts, dur, value as freqValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.freqTrackId}
+            from counter_leading_intervals!((
+              select id, ts, track_id, value
+              from counter
+             where track_id = ${this.config.freqTrackId}
+            ))
           `,
         ),
       );
 
       this.trash.use(
-        await createView(
+        await createPerfettoTable(
           this.trace.engine,
           `raw_idle_${this.trackUuid}`,
           `
@@ -102,8 +115,11 @@
               ts,
               dur,
               iif(value = 4294967295, -1, cast(value as int)) as idleValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.idleTrackId}
+            from counter_leading_intervals!((
+              select id, ts, track_id, value
+              from counter
+              where track_id = ${this.config.idleTrackId}
+            ))
           `,
         ),
       );
diff --git a/ui/src/plugins/dev.perfetto.CpuFreq/index.ts b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
index bd7b96c..ba9b889 100644
--- a/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
+++ b/ui/src/plugins/dev.perfetto.CpuFreq/index.ts
@@ -37,7 +37,7 @@
       from counter c
       join cpu_counter_track t on c.track_id = t.id
       join _counter_track_summary s on t.id = s.id
-      where name = 'cpufreq';
+      where t.type = 'cpu_frequency';
     `);
     const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
 
@@ -48,14 +48,14 @@
           id as cpuFreqId,
           (
             select id
-            from cpu_counter_track
-            where name = 'cpuidle'
-            and cpu = ${cpu}
+            from cpu_counter_track t
+            where t.type = 'cpu_idle'
+              and t.cpu = ${cpu}
             limit 1
           ) as cpuIdleId
-        from cpu_counter_track
+        from cpu_counter_track t
         join _counter_track_summary using (id)
-        where name = 'cpufreq' and cpu = ${cpu}
+        where t.type = 'cpu_frequency' and t.cpu = ${cpu}
         limit 1;
       `);
 
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
index 87d5555..cd70e6d 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_by_process_selection_aggregator.ts
@@ -12,26 +12,31 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {exists} from '../../base/utils';
 import {ColumnDef, Sorting} from '../../public/aggregation';
 import {AreaSelection} from '../../public/selection';
 import {Engine} from '../../trace_processor/engine';
 import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
 import {AreaSelectionAggregator} from '../../public/selection';
+import {Dataset} from '../../trace_processor/dataset';
+import {LONG, NUM} from '../../trace_processor/query_result';
 
 export class CpuSliceByProcessSelectionAggregator
   implements AreaSelectionAggregator
 {
   readonly id = 'cpu_by_process_aggregation';
+  readonly trackKind = CPU_SLICE_TRACK_KIND;
+  readonly schema = {
+    dur: LONG,
+    ts: LONG,
+    utid: NUM,
+  } as const;
 
-  async createAggregateView(engine: Engine, area: AreaSelection) {
-    const selectedCpus: number[] = [];
-    for (const trackInfo of area.tracks) {
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-      }
-    }
-    if (selectedCpus.length === 0) return false;
+  async createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ) {
+    if (!dataset) return false;
 
     await engine.query(`
       create or replace perfetto table ${this.id} as
@@ -41,14 +46,12 @@
         sum(dur) AS total_dur,
         sum(dur) / count() as avg_dur,
         count() as occurrences
-      from sched
+      from (${dataset.query()})
       join thread USING (utid)
       join process USING (upid)
       where
-        cpu in (${selectedCpus})
-        and ts + dur > ${area.start}
+        ts + dur > ${area.start}
         and ts < ${area.end}
-        and utid != 0
       group by upid
     `);
     return true;
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
index 064ba8a..39c6c16 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_selection_aggregator.ts
@@ -12,24 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {exists} from '../../base/utils';
 import {ColumnDef, Sorting} from '../../public/aggregation';
 import {AreaSelection} from '../../public/selection';
 import {CPU_SLICE_TRACK_KIND} from '../../public/track_kinds';
 import {Engine} from '../../trace_processor/engine';
 import {AreaSelectionAggregator} from '../../public/selection';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {Dataset} from '../../trace_processor/dataset';
 
 export class CpuSliceSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'cpu_aggregation';
+  readonly trackKind = CPU_SLICE_TRACK_KIND;
+  readonly schema = {
+    dur: LONG,
+    ts: LONG,
+    utid: NUM,
+  } as const;
 
-  async createAggregateView(engine: Engine, area: AreaSelection) {
-    const selectedCpus: number[] = [];
-    for (const trackInfo of area.tracks) {
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-      }
-    }
-    if (selectedCpus.length === 0) return false;
+  async createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ) {
+    if (!dataset) return false;
 
     await engine.query(`
       create or replace perfetto table ${this.id} as
@@ -43,11 +48,10 @@
         count() as occurrences
       from process
       join thread using (upid)
-      join sched using (utid)
-      where cpu in (${selectedCpus})
-        and sched.ts + sched.dur > ${area.start}
+      join (${dataset.query()}) as sched using (utid)
+      where
+        sched.ts + sched.dur > ${area.start}
         and sched.ts < ${area.end}
-        and utid != 0
       group by utid
     `);
     return true;
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
index 0216121..ab49fa3 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
@@ -22,7 +22,7 @@
   drawTrackHoverTooltip,
 } from '../../base/canvas_utils';
 import {cropText} from '../../base/string_utils';
-import {Color} from '../../public/color';
+import {Color} from '../../base/color';
 import {colorForThread} from '../../components/colorizer';
 import {TrackData} from '../../components/tracks/track_data';
 import {TimelineFetcher} from '../../components/tracks/track_helper';
@@ -96,11 +96,16 @@
 
   getDataset(): Dataset | undefined {
     return new SourceDataset({
-      src: 'select id, ts, dur, cpu from sched where utid != 0',
+      // TODO(stevegolton): Once we allow datasets to have more than one filter,
+      // move this where clause to a dataset filter and change this src to
+      // 'sched'.
+      src: 'select id, ts, dur, cpu, utid from sched where utid != 0',
       schema: {
         id: NUM,
         ts: LONG,
         dur: LONG,
+        cpu: NUM,
+        utid: NUM,
       },
       filter: {
         col: 'cpu',
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
index e626ee3..3edecf3 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
@@ -69,7 +69,6 @@
     }
     const wakeup = await getSchedWakeupInfo(this.trace.engine, sched);
     this.details = {sched, wakeup};
-    this.trace.scheduleFullRedraw();
   }
 
   render() {
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts b/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts
new file mode 100644
index 0000000..111acd2
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/data_visualiser.ts
@@ -0,0 +1,176 @@
+// 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 {SqlTableState} from '../../components/widgets/sql/legacy_table/state';
+import {
+  Chart,
+  ChartOption,
+  createChartConfigFromSqlTableState,
+  renderChartComponent,
+} from '../../components/widgets/charts/chart';
+import {Trace} from '../../public/trace';
+import {Menu, MenuItem, MenuItemAttrs} from '../../widgets/menu';
+import SqlModulesPlugin from '../dev.perfetto.SqlModules';
+import {exists} from '../../base/utils';
+import {Button} from '../../widgets/button';
+import {Icons} from '../../base/semantic_icons';
+import {DetailsShell} from '../../widgets/details_shell';
+import {SqlTable} from '../../components/widgets/sql/legacy_table/table';
+import {AddChartMenuItem} from '../../components/widgets/charts/add_chart_menu';
+import {
+  SplitPanel,
+  SplitPanelDrawerVisibility,
+} from '../../widgets/split_panel';
+import {VerticalSplitContainer} from './vertical_split_container';
+
+export interface DataVisualiserState {
+  sqlTableViewState?: SqlTableState;
+  selectedTableName?: string;
+}
+
+export interface DataVisualiserAttrs {
+  trace: Trace;
+  readonly state: DataVisualiserState;
+  charts: Set<Chart>;
+}
+
+export class DataVisualiser implements m.ClassComponent<DataVisualiserAttrs> {
+  private visibility = SplitPanelDrawerVisibility.VISIBLE;
+
+  // Show menu with standard library tables
+  private renderSelectableTablesMenuItems(
+    trace: Trace,
+    state: DataVisualiserState,
+  ): m.Vnode<MenuItemAttrs, unknown>[] {
+    const sqlModules = trace.plugins
+      .getPlugin(SqlModulesPlugin)
+      .getSqlModules();
+    return sqlModules.listTables().map((tableName) => {
+      const sqlTable = sqlModules
+        .getModuleForTable(tableName)
+        ?.getTable(tableName);
+      const sqlTableViewDescription = sqlModules
+        .getModuleForTable(tableName)
+        ?.getSqlTableDescription(tableName);
+
+      return m(MenuItem, {
+        label: tableName,
+        onclick: () => {
+          if (
+            (state.selectedTableName &&
+              tableName === state.selectedTableName) ||
+            sqlTable === undefined ||
+            sqlTableViewDescription === undefined
+          ) {
+            return;
+          }
+
+          state.selectedTableName = sqlTable.name;
+          state.sqlTableViewState = new SqlTableState(
+            trace,
+            {
+              name: tableName,
+              columns: sqlTable.getTableColumns(),
+            },
+            {imports: sqlTableViewDescription.imports},
+          );
+        },
+      });
+    });
+  }
+
+  private renderSqlTable(state: DataVisualiserState, charts: Set<Chart>) {
+    const sqlTableViewState = state.sqlTableViewState;
+
+    if (sqlTableViewState === undefined) return;
+
+    const range = sqlTableViewState.getDisplayedRange();
+    const rowCount = sqlTableViewState.getTotalRowCount();
+
+    const navigation = [
+      exists(range) &&
+        exists(rowCount) &&
+        `Showing rows ${range.from}-${range.to} of ${rowCount}`,
+      m(Button, {
+        icon: Icons.GoBack,
+        disabled: !sqlTableViewState.canGoBack(),
+        onclick: () => sqlTableViewState!.goBack(),
+      }),
+      m(Button, {
+        icon: Icons.GoForward,
+        disabled: !sqlTableViewState.canGoForward(),
+        onclick: () => sqlTableViewState!.goForward(),
+      }),
+    ];
+
+    return m(
+      DetailsShell,
+      {
+        title: 'Explore Table',
+        buttons: navigation,
+        fillParent: false,
+      },
+      m(SqlTable, {
+        state: sqlTableViewState,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              sqlTableViewState,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => charts.add(chart),
+          }),
+      }),
+    );
+  }
+
+  private renderRemovableChart(chart: Chart, charts: Set<Chart>) {
+    return m(
+      '.chart-card',
+      {
+        key: `${chart.option}-${chart.config.columnTitle}`,
+      },
+      m(Button, {
+        icon: Icons.Close,
+        onclick: () => {
+          charts.delete(chart);
+        },
+      }),
+      renderChartComponent(chart),
+    );
+  }
+
+  view({attrs}: m.CVnode<DataVisualiserAttrs>) {
+    const {trace, state, charts} = attrs;
+
+    return m(
+      SplitPanel,
+      {
+        visibility: this.visibility,
+        onVisibilityChange: (visibility) => {
+          this.visibility = visibility;
+        },
+        drawerContent: this.renderSqlTable(state, charts),
+      },
+      m(VerticalSplitContainer, {
+        leftPane: m(Menu, this.renderSelectableTablesMenuItems(trace, state)),
+        rightPane: Array.from(attrs.charts.values()).map((chart) =>
+          this.renderRemovableChart(chart, attrs.charts),
+        ),
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 8407b4d..5a45b8a 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -15,26 +15,9 @@
 import m from 'mithril';
 import {PageWithTraceAttrs} from '../../public/page';
 import {SqlTableState as SqlTableViewState} from '../../components/widgets/sql/legacy_table/state';
-import {SqlTable as SqlTableView} from '../../components/widgets/sql/legacy_table/table';
-import {exists} from '../../base/utils';
-import {Menu, MenuItem, MenuItemAttrs} from '../../widgets/menu';
-import {Button} from '../../widgets/button';
-import {Icons} from '../../base/semantic_icons';
-import {DetailsShell} from '../../widgets/details_shell';
-import {
-  Chart,
-  ChartOption,
-  createChartConfigFromSqlTableState,
-  renderChartComponent,
-} from '../../components/widgets/charts/chart';
-import {AddChartMenuItem} from '../../components/widgets/charts/add_chart_menu';
-import {
-  SplitPanel,
-  SplitPanelDrawerVisibility,
-} from '../../widgets/split_panel';
-import {Trace} from '../../public/trace';
-import SqlModulesPlugin from '../dev.perfetto.SqlModules';
-import {scheduleFullRedraw} from '../../widgets/raf';
+import {Chart} from '../../components/widgets/charts/chart';
+import {SegmentedButtons} from '../../widgets/segmented_buttons';
+import {DataVisualiser} from './data_visualiser';
 
 export interface ExploreTableState {
   sqlTableViewState?: SqlTableViewState;
@@ -46,114 +29,18 @@
   readonly charts: Set<Chart>;
 }
 
+enum ExplorePageModes {
+  QUERY_BUILDER,
+  DATA_VISUALISER,
+}
+
+const ExplorePageModeToLabel: Record<ExplorePageModes, string> = {
+  [ExplorePageModes.QUERY_BUILDER]: 'Query Builder',
+  [ExplorePageModes.DATA_VISUALISER]: 'Data Visualiser',
+};
+
 export class ExplorePage implements m.ClassComponent<ExplorePageAttrs> {
-  private visibility = SplitPanelDrawerVisibility.VISIBLE;
-
-  // Show menu with standard library tables
-  private renderSelectableTablesMenuItems(
-    trace: Trace,
-    state: ExploreTableState,
-  ): m.Vnode<MenuItemAttrs, unknown>[] {
-    const sqlModules = trace.plugins
-      .getPlugin(SqlModulesPlugin)
-      .getSqlModules();
-    return sqlModules.listTables().map((tableName) => {
-      const sqlTable = sqlModules
-        .getModuleForTable(tableName)
-        ?.getTable(tableName);
-      const sqlTableViewDescription = sqlModules
-        .getModuleForTable(tableName)
-        ?.getSqlTableDescription(tableName);
-
-      return m(MenuItem, {
-        label: tableName,
-        onclick: () => {
-          if (
-            (state.selectedTableName &&
-              tableName === state.selectedTableName) ||
-            sqlTable === undefined ||
-            sqlTableViewDescription === undefined
-          ) {
-            return;
-          }
-
-          state.selectedTableName = sqlTable.name;
-          state.sqlTableViewState = new SqlTableViewState(
-            trace,
-            {
-              name: tableName,
-              columns: sqlTable.getTableColumns(),
-            },
-            {imports: sqlTableViewDescription.imports},
-          );
-        },
-      });
-    });
-  }
-
-  private renderSqlTable(state: ExploreTableState, charts: Set<Chart>) {
-    const sqlTableViewState = state.sqlTableViewState;
-
-    if (sqlTableViewState === undefined) return;
-
-    const range = sqlTableViewState.getDisplayedRange();
-    const rowCount = sqlTableViewState.getTotalRowCount();
-
-    const navigation = [
-      exists(range) &&
-        exists(rowCount) &&
-        `Showing rows ${range.from}-${range.to} of ${rowCount}`,
-      m(Button, {
-        icon: Icons.GoBack,
-        disabled: !sqlTableViewState.canGoBack(),
-        onclick: () => sqlTableViewState!.goBack(),
-      }),
-      m(Button, {
-        icon: Icons.GoForward,
-        disabled: !sqlTableViewState.canGoForward(),
-        onclick: () => sqlTableViewState!.goForward(),
-      }),
-    ];
-
-    return m(
-      DetailsShell,
-      {
-        title: 'Explore Table',
-        buttons: navigation,
-        fillParent: false,
-      },
-      m(SqlTableView, {
-        state: sqlTableViewState,
-        addColumnMenuItems: (column, columnAlias) =>
-          m(AddChartMenuItem, {
-            chartConfig: createChartConfigFromSqlTableState(
-              column,
-              columnAlias,
-              sqlTableViewState,
-            ),
-            chartOptions: [ChartOption.HISTOGRAM],
-            addChart: (chart) => charts.add(chart),
-          }),
-      }),
-    );
-  }
-
-  private renderRemovableChart(chart: Chart, charts: Set<Chart>) {
-    return m(
-      '.chart-card',
-      {
-        key: `${chart.option}-${chart.config.columnTitle}`,
-      },
-      m(Button, {
-        icon: Icons.Close,
-        onclick: () => {
-          charts.delete(chart);
-          scheduleFullRedraw();
-        },
-      }),
-      renderChartComponent(chart),
-    );
-  }
+  private selectedMode = ExplorePageModes.QUERY_BUILDER;
 
   view({attrs}: m.CVnode<ExplorePageAttrs>) {
     const {trace, state, charts} = attrs;
@@ -161,25 +48,25 @@
     return m(
       '.page.explore-page',
       m(
-        SplitPanel,
-        {
-          visibility: this.visibility,
-          onVisibilityChange: (visibility) => {
-            this.visibility = visibility;
-          },
-          drawerContent: this.renderSqlTable(state, charts),
-        },
-        m(
-          '.chart-container',
-          m(Menu, this.renderSelectableTablesMenuItems(trace, state)),
-        ),
-        m(
-          '.chart-container',
-          Array.from(charts.values()).map((chart) =>
-            this.renderRemovableChart(chart, charts),
-          ),
-        ),
+        '.explore-page__header',
+        m('h1', 'Exploration Mode: '),
+        m(SegmentedButtons, {
+          options: [
+            {label: ExplorePageModeToLabel[ExplorePageModes.QUERY_BUILDER]},
+            {label: ExplorePageModeToLabel[ExplorePageModes.DATA_VISUALISER]},
+          ],
+          selectedOption: this.selectedMode,
+          onOptionSelected: (i) => (this.selectedMode = i),
+        }),
       ),
+      this.selectedMode === ExplorePageModes.QUERY_BUILDER &&
+        m('div', 'Query builder goes here'),
+      this.selectedMode === ExplorePageModes.DATA_VISUALISER &&
+        m(DataVisualiser, {
+          trace,
+          state,
+          charts,
+        }),
     );
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
index f4ca569..04167c7 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/styles.scss
@@ -13,9 +13,13 @@
 // limitations under the License.
 .explore-page {
   position: relative;
-  display: flex;
-  flex-direction: column;
   overflow: auto;
+  padding: 0.25rem;
+
+  &__header {
+    display: flex;
+    align-items: center;
+  }
 
   .chart-card {
     border-radius: $pf-border-radius;
@@ -34,11 +38,39 @@
     }
   }
 
-  .chart-container {
-    flex: 1;
-    position: relative;
+  .pf-vertical-split-container {
     display: flex;
-    flex-flow: column nowrap;
-    overflow: auto;
+    flex-direction: row;
+    height: 100%;
+
+    &__left-pane {
+      position: relative;
+      display: flex;
+      flex-direction: row;
+
+      &__content {
+        flex: 1;
+        overflow: auto;
+      }
+
+      &__resize-handle {
+        display: block;
+        height: 100%;
+        width: 5px;
+        background-color: darkgrey;
+
+        // Ensures that the resize-handler is overlayed
+        // on top of content and stays in a fixed
+        // position at the right of the left-pane
+        z-index: 2;
+        position: absolute;
+        right: 0;
+        cursor: col-resize;
+      }
+    }
+
+    &__right-pane {
+      flex: 1;
+    }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts b/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts
new file mode 100644
index 0000000..2d39fde
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/vertical_split_container.ts
@@ -0,0 +1,83 @@
+// 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 {DisposableStack} from '../../base/disposable_stack';
+import {DragGestureHandler} from '../../base/drag_gesture_handler';
+import {assertExists} from '../../base/logging';
+
+interface VerticalSplitContainerAttrs {
+  leftPane: m.Children;
+  rightPane: m.Children;
+}
+
+export class VerticalSplitContainer
+  implements m.ClassComponent<VerticalSplitContainerAttrs>
+{
+  // Note: For BEM class names (https://getbem.com/)
+  private readonly leftPaneClassName =
+    '.pf-vertical-split-container__left-pane';
+  private readonly leftPaneResizeHandle =
+    this.leftPaneClassName + '__resize-handle';
+  private readonly rightPaneClassName =
+    '.pf-vertical-split-container__right-pane';
+
+  private readonly trash = new DisposableStack();
+  private leftPaneWidth = 0;
+  private rightPaneWidth = 0;
+
+  oncreate({dom}: m.VnodeDOM<VerticalSplitContainerAttrs, this>) {
+    const leftPane = assertExists(
+      dom.querySelector(this.leftPaneClassName),
+    ) as HTMLElement;
+    const rightPane = assertExists(
+      dom.querySelector(this.rightPaneClassName),
+    ) as HTMLElement;
+
+    this.trash.use(
+      new DragGestureHandler(
+        assertExists(
+          dom.querySelector(this.leftPaneResizeHandle),
+        ) as HTMLElement,
+        /* onDrag */
+        (x, _y) => {
+          leftPane.style.width = `${this.leftPaneWidth + x}px`;
+          rightPane.style.width = `${this.rightPaneWidth - x}px`;
+        },
+        /* onDragStarted */
+        () => {
+          this.leftPaneWidth = leftPane.clientWidth;
+        },
+        /* onDragFinished */
+        () => {},
+      ),
+    );
+  }
+
+  onremove(): void {
+    this.trash.dispose();
+  }
+
+  view({attrs}: m.VnodeDOM<VerticalSplitContainerAttrs, this>) {
+    return m(
+      '.pf-vertical-split-container',
+      m(
+        this.leftPaneClassName,
+        m(this.leftPaneClassName + '__content', attrs.leftPane),
+        m(this.leftPaneResizeHandle),
+      ),
+      m(this.rightPaneClassName, attrs.rightPane),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
index 7bad7dd..ac6dc8e 100644
--- a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -12,18 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
 import {makeColorScheme} from '../../components/colorizer';
-import {ColorScheme} from '../../public/color_scheme';
+import {ColorScheme} from '../../base/color_scheme';
 import {
   NAMED_ROW,
   NamedSliceTrack,
 } from '../../components/tracks/named_slice_track';
 import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../components/tracks/slice_layout';
-import {STR_NULL} from '../../trace_processor/query_result';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
 import {Slice} from '../../public/track';
 import {Trace} from '../../public/trace';
 import {TrackEventDetails} from '../../public/selection';
+import {SourceDataset} from '../../trace_processor/dataset';
 
 // color named and defined based on Material Design color palettes
 // 500 colors indicate a timeline slice is not a partial jank (not a jank or
@@ -106,13 +107,23 @@
     };
   }
 
-  // Override dataset from base class NamedSliceTrack as we don't want these
-  // tracks to participate in generic area selection aggregation (frames tracks
-  // have their own dedicated aggregation panel).
-  // TODO(stevegolton): In future CLs this will be handled with aggregation keys
-  // instead, as this track will have to expose a dataset anyway.
   override getDataset() {
-    return undefined;
+    return new SourceDataset({
+      src: 'actual_frame_timeline_slice',
+      schema: {
+        id: NUM,
+        // Don't expose name to avoid this track getting selected by the generic
+        // slice aggregator, which is useless for frames tracks.
+        // name: STR,
+        ts: LONG,
+        dur: LONG,
+        jank_type: STR,
+      },
+      filter: {
+        col: 'track_id',
+        in: this.trackIds,
+      },
+    });
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
index e9d7586..921f620 100644
--- a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
 import {makeColorScheme} from '../../components/colorizer';
 import {
   NAMED_ROW,
diff --git a/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
index 6bf4eba..98b1415 100644
--- a/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/frame_selection_aggregator.ts
@@ -17,19 +17,25 @@
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../public/track_kinds';
 import {Engine} from '../../trace_processor/engine';
 import {AreaSelectionAggregator} from '../../public/selection';
+import {LONG, STR} from '../../trace_processor/query_result';
+import {Dataset} from '../../trace_processor/dataset';
 
 export class FrameSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'frame_aggregation';
+  readonly priority = 1;
+  readonly schema = {
+    ts: LONG,
+    dur: LONG,
+    jank_type: STR,
+  } as const;
+  readonly trackKind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
 
-  async createAggregateView(engine: Engine, area: AreaSelection) {
-    const selectedSqlTrackIds: number[] = [];
-    for (const trackInfo of area.tracks) {
-      if (trackInfo?.tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
-        trackInfo.tags.trackIds &&
-          selectedSqlTrackIds.push(...trackInfo.tags.trackIds);
-      }
-    }
-    if (selectedSqlTrackIds.length === 0) return false;
+  async createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ) {
+    if (!dataset) return false;
 
     await engine.query(`
       create or replace perfetto table ${this.id} as
@@ -39,9 +45,8 @@
         min(dur) as minDur,
         avg(dur) as meanDur,
         max(dur) as maxDur
-      from actual_frame_timeline_slice
-      where track_id in (${selectedSqlTrackIds})
-        AND ts + dur > ${area.start}
+      from (${dataset.query()})
+      where ts + dur > ${area.start}
         AND ts < ${area.end}
       group by jank_type
     `);
diff --git a/ui/src/plugins/dev.perfetto.Frames/index.ts b/ui/src/plugins/dev.perfetto.Frames/index.ts
index a163da9..3e63963 100644
--- a/ui/src/plugins/dev.perfetto.Frames/index.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/index.ts
@@ -18,14 +18,18 @@
 } from '../../public/track_kinds';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
-import {getTrackName} from '../../public/utils';
 import {TrackNode} from '../../public/workspace';
-import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {NUM, STR} from '../../trace_processor/query_result';
 import {ActualFramesTrack} from './actual_frames_track';
 import {ExpectedFramesTrack} from './expected_frames_track';
 import {FrameSelectionAggregator} from './frame_selection_aggregator';
 import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
+// Build a standardized URI for a frames track
+function makeUri(upid: number, kind: 'expected_frames' | 'actual_frames') {
+  return `/process_${upid}/${kind}`;
+}
+
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.Frames';
   static readonly dependencies = [ProcessThreadGroupsPlugin];
@@ -36,50 +40,80 @@
     ctx.selection.registerAreaSelectionAggregator(
       new FrameSelectionAggregator(),
     );
+
+    ctx.selection.registerSqlSelectionResolver({
+      sqlTableName: 'slice',
+      callback: async (id: number) => {
+        const result = await ctx.engine.query(`
+          select
+            process_track.type as trackType,
+            process_track.upid as upid
+          from slice
+          join process_track on slice.track_id = process_track.id
+          where
+            slice.id = ${id}
+            and process_track.type in (
+              'android_expected_frame_timeline',
+              'android_actual_frame_timeline'
+            )
+        `);
+
+        if (result.numRows() === 0) {
+          return undefined;
+        }
+
+        const {trackType, upid} = result.firstRow({
+          trackType: STR,
+          upid: NUM,
+        });
+
+        const suffix =
+          trackType === 'expected_frame_timeline'
+            ? 'expected_frames'
+            : 'actual_frames';
+
+        return {
+          trackUri: makeUri(upid, suffix),
+          eventId: id,
+        };
+      },
+    });
   }
 
   async addExpectedFrames(ctx: Trace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
+      with summary as (
+        select
+          pt.upid,
+          group_concat(id) AS track_ids,
+          count() AS track_count
+        from process_track pt
+        join _slice_track_summary USING (id)
+        where pt.type = 'android_expected_frame_timeline'
+        group by pt.upid
+      )
       select
-        upid,
-        t.name as trackName,
+        t.upid,
         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_parent_id_and_name t
-      join process using(upid)
-      where t.name = "Expected Timeline"
+      from summary t
     `);
 
     const it = result.iter({
       upid: NUM,
-      trackName: STR_NULL,
       trackIds: STR,
-      processName: STR_NULL,
-      pid: NUM_NULL,
       maxDepth: NUM,
     });
 
     for (; it.valid(); it.next()) {
       const upid = it.upid;
-      const trackName = it.trackName;
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      const processName = it.processName;
-      const pid = it.pid;
       const maxDepth = it.maxDepth;
 
-      const title = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind: 'ExpectedFrames',
-      });
-
-      const uri = `/process_${upid}/expected_frames`;
+      const title = 'Expected Timeline';
+      const uri = makeUri(upid, 'expected_frames');
       ctx.tracks.registerTrack({
         uri,
         title,
@@ -101,50 +135,36 @@
   async addActualFrames(ctx: Trace): Promise<void> {
     const {engine} = ctx;
     const result = await engine.query(`
+      with summary as (
+        select
+          pt.upid,
+          group_concat(id) AS track_ids,
+          count() AS track_count
+        from process_track pt
+        join _slice_track_summary USING (id)
+        where pt.type = 'android_actual_frame_timeline'
+        group by pt.upid
+      )
       select
-        upid,
-        t.name as trackName,
+        t.upid,
         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_parent_id_and_name t
-      join process using(upid)
-      where t.name = "Actual Timeline"
+      from summary t
     `);
 
     const it = result.iter({
       upid: NUM,
-      trackName: STR_NULL,
       trackIds: STR,
-      processName: STR_NULL,
-      pid: NUM_NULL,
-      maxDepth: NUM_NULL,
+      maxDepth: NUM,
     });
     for (; it.valid(); it.next()) {
       const upid = it.upid;
-      const trackName = it.trackName;
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
-      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 kind = 'ActualFrames';
-      const title = getTrackName({
-        name: trackName,
-        upid,
-        pid,
-        processName,
-        kind,
-      });
-
-      const uri = `/process_${upid}/actual_frames`;
+      const title = 'Actual Timeline';
+      const uri = makeUri(upid, 'actual_frames');
       ctx.tracks.registerTrack({
         uri,
         title,
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
index 8083fb3..31592d3 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
@@ -174,7 +174,6 @@
         this.pagination.count,
         attrs.filterStore.state,
       );
-      attrs.trace.scheduleFullRedraw();
     });
   }
 
diff --git a/ui/src/plugins/dev.perfetto.GpuFreq/index.ts b/ui/src/plugins/dev.perfetto.GpuFreq/index.ts
new file mode 100644
index 0000000..19a24c0
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.GpuFreq/index.ts
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 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 {TrackNode} from '../../public/workspace';
+import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM} from '../../trace_processor/query_result';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack/index';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.GpuFreq';
+  static readonly dependencies = [TraceProcessorTrackPlugin];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const result = await ctx.engine.query(`
+      select id, gpu_id as gpuId
+      from gpu_counter_track
+      join _counter_track_summary using (id)
+      where name = 'gpufreq'
+    `);
+    const it = result.iter({id: NUM, gpuId: NUM});
+    for (; it.valid(); it.next()) {
+      const uri = `/gpu_frequency_${it.gpuId}`;
+      const name = `Gpu ${it.gpuId} Frequency`;
+      ctx.tracks.registerTrack({
+        uri,
+        title: name,
+        tags: {
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [it.id],
+        },
+        track: new TraceProcessorCounterTrack(ctx, uri, {}, it.id, name),
+      });
+      const track = new TrackNode({uri, title: name, sortOrder: -20});
+      ctx.workspace.addChildInOrder(track);
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
index 6805471..45f0a92 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
@@ -106,7 +106,6 @@
                 intent: Intent.Primary,
                 onclick: () => {
                   downloadPprof(this.trace, this.upid, ts);
-                  this.trace.scheduleFullRedraw();
                 },
               }),
           ],
@@ -144,7 +143,6 @@
           text: 'Skip',
           action: () => {
             this.flamegraphModalDismissed = true;
-            trace.scheduleFullRedraw();
           },
         },
       ],
@@ -329,6 +327,8 @@
       ];
     case ProfileType.PERF_SAMPLE:
       throw new Error('Perf sample not supported');
+    case ProfileType.INSTRUMENTS_SAMPLE:
+      throw new Error('Instruments sample not supported');
   }
 }
 
@@ -396,6 +396,9 @@
     case ProfileType.PERF_SAMPLE:
       assertFalse(false, 'Perf sample not supported');
       return 'Impossible';
+    case ProfileType.INSTRUMENTS_SAMPLE:
+      assertFalse(false, 'Instruments sample not supported');
+      return 'Impossible';
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
new file mode 100644
index 0000000..7fddaf8
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/index.ts
@@ -0,0 +1,135 @@
+// Copyright (C) 2025 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 {TrackData} from '../../components/tracks/track_data';
+import {INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND} from '../../public/track_kinds';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {assertExists} from '../../base/logging';
+import {
+  ProcessInstrumentsSamplesProfileTrack,
+  ThreadInstrumentsSamplesProfileTrack,
+} from './instruments_samples_profile_track';
+import {getThreadUriPrefix} from '../../public/utils';
+import {TrackNode} from '../../public/workspace';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+
+export interface Data extends TrackData {
+  tsStarts: BigInt64Array;
+}
+
+function makeUriForProc(upid: number) {
+  return `/process_${upid}/instruments_samples_profile`;
+}
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.InstrumentsSamplesProfile';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const pResult = await ctx.engine.query(`
+      select distinct upid
+      from instruments_sample
+      join thread using (utid)
+      where callsite_id is not null and upid is not null
+    `);
+    for (const it = pResult.iter({upid: NUM}); it.valid(); it.next()) {
+      const upid = it.upid;
+      const uri = makeUriForProc(upid);
+      const title = `Process Callstacks`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
+          upid,
+        },
+        track: new ProcessInstrumentsSamplesProfileTrack(ctx, uri, upid),
+      });
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
+      const track = new TrackNode({uri, title, sortOrder: -40});
+      group?.addChildInOrder(track);
+    }
+    const tResult = await ctx.engine.query(`
+      select distinct
+        utid,
+        tid,
+        thread.name as threadName,
+        upid
+      from instruments_sample
+      join thread using (utid)
+      where callsite_id is not null
+    `);
+    for (
+      const it = tResult.iter({
+        utid: NUM,
+        tid: NUM,
+        threadName: STR_NULL,
+        upid: NUM_NULL,
+      });
+      it.valid();
+      it.next()
+    ) {
+      const {threadName, utid, tid, upid} = it;
+      const title =
+        threadName === null
+          ? `Thread Callstacks ${tid}`
+          : `${threadName} Callstacks ${tid}`;
+      const uri = `${getThreadUriPrefix(upid, utid)}_instruments_samples_profile`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND,
+          utid,
+          upid: upid ?? undefined,
+        },
+        track: new ThreadInstrumentsSamplesProfileTrack(ctx, uri, utid),
+      });
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForThread(utid);
+      const track = new TrackNode({uri, title, sortOrder: -50});
+      group?.addChildInOrder(track);
+    }
+
+    ctx.onTraceReady.addListener(async () => {
+      await selectInstrumentsSample(ctx);
+    });
+  }
+}
+
+async function selectInstrumentsSample(ctx: Trace) {
+  const profile = await assertExists(ctx.engine).query(`
+    select upid
+    from instruments_sample
+    join thread using (utid)
+    where callsite_id is not null
+    order by ts desc
+    limit 1
+  `);
+  if (profile.numRows() !== 1) return;
+  const row = profile.firstRow({upid: NUM});
+  const upid = row.upid;
+
+  // Create an area selection over the first process with a instruments samples track
+  ctx.selection.selectArea({
+    start: ctx.traceInfo.start,
+    end: ctx.traceInfo.end,
+    trackUris: [makeUriForProc(upid)],
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts
new file mode 100644
index 0000000..1716c65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.InstrumentsSamplesProfile/instruments_samples_profile_track.ts
@@ -0,0 +1,287 @@
+// Copyright (C) 2025 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 {NUM} from '../../trace_processor/query_result';
+import {Slice} from '../../public/track';
+import {BaseSliceTrack} from '../../components/tracks/base_slice_track';
+import {NAMED_ROW, NamedRow} from '../../components/tracks/named_slice_track';
+import {getColorForSample} from '../../components/colorizer';
+import {
+  ProfileType,
+  TrackEventDetails,
+  TrackEventSelection,
+} from '../../public/selection';
+import {assertExists} from '../../base/logging';
+import {
+  metricsFromTableOrSubquery,
+  QueryFlamegraph,
+} from '../../components/query_flamegraph';
+import {DetailsShell} from '../../widgets/details_shell';
+import {Timestamp} from '../../components/widgets/timestamp';
+import {time} from '../../base/time';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
+import {Trace} from '../../public/trace';
+
+interface InstrumentsSampleRow extends NamedRow {
+  callsiteId: number;
+}
+
+abstract class BaseInstrumentsSamplesProfileTrack extends BaseSliceTrack<
+  Slice,
+  InstrumentsSampleRow
+> {
+  constructor(trace: Trace, uri: string) {
+    super(trace, uri);
+  }
+
+  protected getRowSpec(): InstrumentsSampleRow {
+    return {...NAMED_ROW, callsiteId: NUM};
+  }
+
+  protected rowToSlice(row: InstrumentsSampleRow): Slice {
+    const baseSlice = super.rowToSliceBase(row);
+    const name = assertExists(row.name);
+    const colorScheme = getColorForSample(row.callsiteId);
+    return {...baseSlice, title: name, colorScheme};
+  }
+
+  onUpdatedSlices(slices: Slice[]) {
+    for (const slice of slices) {
+      slice.isHighlighted = slice === this.hoveredSlice;
+    }
+  }
+}
+
+export class ProcessInstrumentsSamplesProfileTrack extends BaseInstrumentsSamplesProfileTrack {
+  constructor(
+    trace: Trace,
+    uri: string,
+    private readonly upid: number,
+  ) {
+    super(trace, uri);
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Instruments Sample' as name,
+        callsite_id as callsiteId
+      from instruments_sample p
+      join thread using (utid)
+      where upid = ${this.upid} and callsite_id is not null
+      order by ts
+    `;
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const details = await super.getSelectionDetails(id);
+    if (details === undefined) return undefined;
+    return {
+      ...details,
+      upid: this.upid,
+      profileType: ProfileType.INSTRUMENTS_SAMPLE,
+    };
+  }
+
+  detailsPanel(sel: TrackEventSelection) {
+    const upid = assertExists(sel.upid);
+    const ts = sel.ts;
+
+    const metrics = metricsFromTableOrSubquery(
+      `
+        (
+          select
+            id,
+            parent_id as parentId,
+            name,
+            mapping_name,
+            source_file,
+            cast(line_number AS text) as line_number,
+            self_count
+          from _callstacks_for_callsites!((
+            select p.callsite_id
+            from instruments_sample p
+            join thread t using (utid)
+            where p.ts >= ${ts}
+              and p.ts <= ${ts}
+              and t.upid = ${upid}
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Instruments Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module appleos.instruments.samples',
+      [{name: 'mapping_name', displayName: 'Mapping'}],
+      [
+        {
+          name: 'source_file',
+          displayName: 'Source File',
+          mergeAggregation: 'ONE_OR_NULL',
+        },
+        {
+          name: 'line_number',
+          displayName: 'Line Number',
+          mergeAggregation: 'ONE_OR_NULL',
+        },
+      ],
+    );
+    const serialization = {
+      schema: FLAMEGRAPH_STATE_SCHEMA,
+      state: Flamegraph.createDefaultState(metrics),
+    };
+    const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
+    return {
+      render: () => renderDetailsPanel(flamegraph, ts),
+      serialization,
+    };
+  }
+}
+
+export class ThreadInstrumentsSamplesProfileTrack extends BaseInstrumentsSamplesProfileTrack {
+  constructor(
+    trace: Trace,
+    uri: string,
+    private readonly utid: number,
+  ) {
+    super(trace, uri);
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Instruments Sample' as name,
+        callsite_id as callsiteId
+      from instruments_sample p
+      where utid = ${this.utid} and callsite_id is not null
+      order by ts
+    `;
+  }
+
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const details = await super.getSelectionDetails(id);
+    if (details === undefined) return undefined;
+    return {
+      ...details,
+      utid: this.utid,
+      profileType: ProfileType.INSTRUMENTS_SAMPLE,
+    };
+  }
+
+  detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
+    const utid = assertExists(sel.utid);
+    const ts = sel.ts;
+
+    const metrics = metricsFromTableOrSubquery(
+      `
+        (
+          select
+            id,
+            parent_id as parentId,
+            name,
+            mapping_name,
+            source_file,
+            cast(line_number AS text) as line_number,
+            self_count
+          from _callstacks_for_callsites!((
+            select p.callsite_id
+            from instruments_sample p
+            where p.ts >= ${ts}
+              and p.ts <= ${ts}
+              and p.utid = ${utid}
+          ))
+        )
+      `,
+      [
+        {
+          name: 'Instruments Samples',
+          unit: '',
+          columnName: 'self_count',
+        },
+      ],
+      'include perfetto module appleos.instruments.samples',
+      [{name: 'mapping_name', displayName: 'Mapping'}],
+      [
+        {
+          name: 'source_file',
+          displayName: 'Source File',
+          mergeAggregation: 'ONE_OR_NULL',
+        },
+        {
+          name: 'line_number',
+          displayName: 'Line Number',
+          mergeAggregation: 'ONE_OR_NULL',
+        },
+      ],
+    );
+    const serialization = {
+      schema: FLAMEGRAPH_STATE_SCHEMA,
+      state: Flamegraph.createDefaultState(metrics),
+    };
+    const flamegraph = new QueryFlamegraph(this.trace, metrics, serialization);
+    return {
+      render: () => renderDetailsPanel(flamegraph, ts),
+      serialization,
+    };
+  }
+}
+
+function renderDetailsPanel(flamegraph: QueryFlamegraph, ts: time) {
+  return m(
+    '.flamegraph-profile',
+    m(
+      DetailsShell,
+      {
+        fillParent: true,
+        title: m('.title', 'Instruments Samples'),
+        description: [],
+        buttons: [
+          m(
+            'div.time',
+            `First timestamp: `,
+            m(Timestamp, {
+              ts,
+            }),
+          ),
+          m(
+            'div.time',
+            `Last timestamp: `,
+            m(Timestamp, {
+              ts,
+            }),
+          ),
+        ],
+      },
+      flamegraph.render(),
+    ),
+  );
+}
diff --git a/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
index ffef258..6a1e53b 100644
--- a/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
+++ b/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts
@@ -136,12 +136,8 @@
             this._result = errResult(e);
             this._json = {};
           }
-        })
-        .finally(() => {
-          this.trace.scheduleFullRedraw();
         });
     }
-    this.trace.scheduleFullRedraw();
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
index 20e1803..b995017 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/index.ts
@@ -27,6 +27,8 @@
   ProcessSummaryTrack,
 } from './process_summary_track';
 import ThreadPlugin from '../dev.perfetto.Thread';
+import {createPerfettoIndex} from '../../trace_processor/sql_utils';
+import {uuidv4Sql} from '../../base/uuid';
 
 // This plugin is responsible for adding summary tracks for process and thread
 // groups.
@@ -40,10 +42,25 @@
   }
 
   private async addProcessTrackGroups(ctx: Trace): Promise<void> {
+    // Makes the queries in `ProcessSchedulingTrack` significantly faster.
+    // TODO(lalitm): figure out a better way to do this without hardcoding this
+    // here.
+    await createPerfettoIndex(
+      ctx.engine,
+      `__process_scheduling_${uuidv4Sql()}`,
+      `__intrinsic_sched_slice(utid)`,
+    );
+    // Makes the queries in `ProcessSummaryTrack` significantly faster.
+    // TODO(lalitm): figure out a better way to do this without hardcoding this
+    // here.
+    await createPerfettoIndex(
+      ctx.engine,
+      `__process_summary_${uuidv4Sql()}`,
+      `__intrinsic_slice(track_id)`,
+    );
+
     const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
-
     const cpuCount = Math.max(...ctx.traceInfo.cpus, -1) + 1;
-
     const result = await ctx.engine.query(`
       INCLUDE PERFETTO MODULE android.process_metadata;
 
@@ -87,8 +104,7 @@
         join thread using (utid)
         where upid is null
       )
-  `);
-
+    `);
     const it = result.iter({
       upid: NUM_NULL,
       utid: NUM_NULL,
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
index 53d0519..bf4d29d 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_scheduling_track.ts
@@ -17,7 +17,7 @@
 import {assertExists, assertTrue} from '../../base/logging';
 import {duration, time, Time} from '../../base/time';
 import {drawTrackHoverTooltip} from '../../base/canvas_utils';
-import {Color} from '../../public/color';
+import {Color} from '../../base/color';
 import {colorForThread} from '../../components/colorizer';
 import {TrackData} from '../../components/tracks/track_data';
 import {TimelineFetcher} from '../../components/tracks/track_helper';
@@ -29,6 +29,11 @@
 import {Point2D} from '../../base/geom';
 import {Trace} from '../../public/trace';
 import {ThreadMap} from '../dev.perfetto.Thread/threads';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {
+  createPerfettoTable,
+  createVirtualTable,
+} from '../../trace_processor/sql_utils';
 
 export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
 
@@ -67,44 +72,70 @@
   ) {}
 
   async onCreate(): Promise<void> {
-    if (this.config.upid !== null) {
-      await this.trace.engine.query(`
-        create virtual table process_scheduling_${this.trackUuid}
-        using __intrinsic_slice_mipmap((
+    const getQuery = () => {
+      if (this.config.upid !== null) {
+        // TODO(lalitm): remove the harcoding of the cross join here.
+        return `
           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
+            s.id,
+            s.ts,
+            s.dur,
+            s.cpu
+          from thread t
+          cross join sched s using (utid)
           where
-            utid != 0 and
-            upid = ${this.config.upid}
-        ));
-      `);
-    } else {
+            s.utid != 0 and
+            t.upid = ${this.config.upid}
+          order by ts
+        `;
+      }
       assertExists(this.config.utid);
-      await this.trace.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}
-        ));
-      `);
-    }
+      return `
+        select
+          s.id,
+          s.ts,
+          s.dur,
+          s.cpu
+        from sched s
+        where
+          s.utid = ${this.config.utid}
+      `;
+    };
+
+    const trash = new AsyncDisposableStack();
+    trash.use(
+      await createPerfettoTable(
+        this.trace.engine,
+        `tmp_${this.trackUuid}`,
+        getQuery(),
+      ),
+    );
+    await createVirtualTable(
+      this.trace.engine,
+      `process_scheduling_${this.trackUuid}`,
+      `__intrinsic_slice_mipmap((
+        select
+          s.id,
+          s.ts,
+          iif(
+            s.dur = -1,
+            ifnull(
+              (
+                select n.ts
+                from tmp_${this.trackUuid} n
+                where n.ts > s.ts and n.cpu = s.cpu
+                order by ts
+                limit 1
+              ),
+              trace_end()
+            ) - s.ts,
+            s.dur
+          ) as dur,
+          s.cpu as depth
+        from tmp_${this.trackUuid} s
+      ))`,
+    );
+    await trash.asyncDispose();
   }
 
   async onUpdate({
diff --git a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
index 50c73eb..2c671dd 100644
--- a/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessSummary/process_summary_track.ts
@@ -24,6 +24,11 @@
 import {LONG, NUM} from '../../trace_processor/query_result';
 import {uuidv4Sql} from '../../base/uuid';
 import {TrackRenderContext} from '../../public/track';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {
+  createPerfettoTable,
+  createVirtualTable,
+} from '../../trace_processor/sql_utils';
 
 export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
 
@@ -55,50 +60,61 @@
   }
 
   async onCreate(): Promise<void> {
-    let trackIdQuery: string;
-    if (this.config.upid !== null) {
-      trackIdQuery = `
-        select tt.id as track_id
-        from thread_track as tt
-        join _thread_available_info_summary using (utid)
-        join thread using (utid)
-        where thread.upid = ${this.config.upid}
-        order by slice_count desc
-      `;
-    } else {
-      trackIdQuery = `
+    const getQuery = () => {
+      if (this.config.upid !== null) {
+        return `
+          select tt.id as track_id
+          from thread_track as tt
+          join _thread_available_info_summary using (utid)
+          join thread using (utid)
+          where thread.upid = ${this.config.upid}
+          order by slice_count desc
+        `;
+      }
+      return `
         select tt.id as track_id
         from thread_track as tt
         join _thread_available_info_summary using (utid)
         where tt.utid = ${assertExists(this.config.utid)}
         order by slice_count desc
       `;
-    }
-    await this.engine.query(`
-      create virtual table process_summary_${this.uuid}
-      using __intrinsic_counter_mipmap((
-        with
-          tt as materialized (
-            ${trackIdQuery}
-          ),
-          ss as (
-            select ts, 1.0 as value
-            from slice
-            join tt using (track_id)
-            where slice.depth = 0
-            union all
-            select ts + dur as ts, -1.0 as value
-            from slice
-            join tt using (track_id)
-            where slice.depth = 0
-          )
+    };
+
+    const trash = new AsyncDisposableStack();
+    trash.use(
+      await createPerfettoTable(this.engine, `tmp_${this.uuid}`, getQuery()),
+    );
+    trash.use(
+      await createPerfettoTable(
+        this.engine,
+        `changes_${this.uuid}`,
+        `
+          select ts, 1.0 as value
+          from tmp_${this.uuid}
+          cross join slice using (track_id)
+          where slice.depth = 0
+          union all
+          select ts + dur as ts, -1.0 as value
+          from tmp_${this.uuid}
+          cross join slice using (track_id)
+          where slice.depth = 0
+        `,
+      ),
+    );
+    await createVirtualTable(
+      this.engine,
+      `process_summary_${this.uuid}`,
+      `__intrinsic_counter_mipmap((
         select
           ts,
-          sum(value) over (order by ts) / (select count() from tt) as value
-        from ss
+          sum(value) over (order by ts) / (
+            select count() from tmp_${this.uuid}
+          ) as value
+        from changes_${this.uuid}
         order by ts
-      ));
-    `);
+      ))`,
+    );
+    await trash.asyncDispose();
   }
 
   async onUpdate({
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
index a98c987..07f3121 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts
@@ -77,7 +77,6 @@
                 vnode.attrs.index,
                 !vnode.attrs.entry.starred,
               );
-              vnode.attrs.trace.scheduleFullRedraw();
             },
           },
           m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}),
@@ -101,7 +100,6 @@
           {
             onclick: () => {
               queryHistoryStorage.remove(vnode.attrs.index);
-              vnode.attrs.trace.scheduleFullRedraw();
             },
           },
           m(Icon, {icon: 'delete'}),
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
index 62a169e..05ac152 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_page.ts
@@ -58,7 +58,6 @@
         return;
       }
       state.queryResult = resp;
-      trace.scheduleFullRedraw();
     },
   );
 }
@@ -97,7 +96,6 @@
 
       onUpdate: (text: string) => {
         state.enteredText = text;
-        attrs.trace.scheduleFullRedraw();
       },
     });
   }
@@ -131,7 +129,6 @@
         setQuery: (q: string) => {
           state.enteredText = q;
           state.generation++;
-          attrs.trace.scheduleFullRedraw();
         },
       }),
     );
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
index e0c5a1f..2eac7ef 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
@@ -14,43 +14,39 @@
 
 import m from 'mithril';
 import {RecordPage} from './record_page';
-import {RecordPageV2} from './record_page_v2';
 import {App} from '../../public/app';
 import {PerfettoPlugin} from '../../public/plugin';
-import {RecordingPageController} from './recordingV2/recording_page_controller';
 import {RecordingManager} from './recording_manager';
 import {PageAttrs} from '../../public/page';
 import {bindMithrilAttrs} from '../../base/mithril_utils';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.RecordTrace';
+  static useRecordingV2 = false;
 
   static onActivate(app: App) {
+    const RECORDING_V2_FLAG = app.featureFlags.register({
+      id: 'recordingv2',
+      name: 'Recording V2',
+      description: 'Record using V2 interface',
+      defaultValue: true,
+    });
+    this.useRecordingV2 = RECORDING_V2_FLAG.get();
+    if (this.useRecordingV2) return;
+
     app.sidebar.addMenuItem({
       section: 'navigation',
-      text: 'Record new trace',
+      text: 'Record new trace (legacy)',
       href: '#!/record',
       icon: 'fiber_smart_record',
       sortOrder: 2,
     });
 
-    const RECORDING_V2_FLAG = app.featureFlags.register({
-      id: 'recordingv2',
-      name: 'Recording V2',
-      description: 'Record using V2 interface',
-      defaultValue: false,
+    const recMgr = new RecordingManager(app);
+    const page: m.ClassComponent<PageAttrs> = bindMithrilAttrs(RecordPage, {
+      app,
+      recMgr,
     });
-    const useRecordingV2 = RECORDING_V2_FLAG.get();
-
-    const recMgr = new RecordingManager(app, useRecordingV2);
-    let page: m.ClassComponent<PageAttrs>;
-    if (useRecordingV2) {
-      const recCtl = new RecordingPageController(app, recMgr);
-      recCtl.initFactories();
-      page = bindMithrilAttrs(RecordPageV2, {app, recCtl, recMgr});
-    } else {
-      page = bindMithrilAttrs(RecordPage, {app, recMgr});
-    }
     app.pages.registerPage({route: '/record', traceless: true, page});
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
index 9ac37cb..e1370c7 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -42,7 +42,6 @@
 import {RecordConfig} from './record_config_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
 import {RecordingManager} from './recording_manager';
-import {scheduleFullRedraw} from '../../widgets/raf';
 import {App} from '../../public/app';
 
 type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
@@ -221,7 +220,7 @@
   refreshOnStateChange() {
     // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
     // of posting a custom extension message to retrieve the category list.
-    scheduleFullRedraw();
+    this.app.raf.scheduleFullRedraw();
     if (this.state.fetchChromeCategories && !this.fetchedCategories) {
       this.fetchedCategories = true;
       if (this.state.extensionInstalled) {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
index 021a5db..51cb720 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -47,7 +47,6 @@
 import {RecordingSettings} from './recording_settings';
 import {EtwSettings} from './etw_settings';
 import {RecordingManager} from './recording_manager';
-import {scheduleFullRedraw} from '../../widgets/raf';
 import {App} from '../../public/app';
 import {GcsUploader, BUCKET_NAME, MIME_JSON} from '../../base/gcs_uploader';
 import {showModal} from '../../widgets/modal';
@@ -152,7 +151,6 @@
 
   recMgr.setRecordingTarget(recordingTarget);
   recordTargetStore.save(target);
-  scheduleFullRedraw();
 }
 
 function Instructions(recMgr: RecordingManager, cssClass: string) {
@@ -195,7 +193,6 @@
       disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
       onclick: () => {
         recMgr.setRecordConfig(config, configType);
-        scheduleFullRedraw();
       },
     },
     m('i.material-icons', 'file_upload'),
@@ -241,7 +238,6 @@
                   type: 'NAMED',
                   name: item.title,
                 });
-                scheduleFullRedraw();
               }
             },
           },
@@ -254,7 +250,6 @@
             title: 'Remove configuration',
             onclick: () => {
               recordConfigStore.delete(item.key);
-              scheduleFullRedraw();
             },
           },
           m('i.material-icons', 'delete'),
@@ -289,7 +284,6 @@
         placeholder: 'Title for config',
         oninput() {
           ConfigTitleState.setTitle(this.value);
-          scheduleFullRedraw();
         },
       }),
       m(
@@ -305,7 +299,6 @@
               recMgr.state.recordConfig,
               ConfigTitleState.getTitle(),
             );
-            scheduleFullRedraw();
             ConfigTitleState.clearTitle();
           },
         },
@@ -323,7 +316,6 @@
               )
             ) {
               recMgr.clearRecordConfig();
-              scheduleFullRedraw();
             }
           },
         },
@@ -570,7 +562,6 @@
 
 function onStartRecordingPressed(recMgr: RecordingManager) {
   location.href = '#!/record/instructions';
-  scheduleFullRedraw();
   autosaveConfigStore.save(recMgr.state.recordConfig);
 
   const target = recMgr.state.recordingTarget;
@@ -739,7 +730,8 @@
     '.record-menu',
     {
       class: recInProgress ? 'disabled' : '',
-      onclick: () => scheduleFullRedraw(),
+      // Just setting this event handler will trigger redraws.
+      onclick: () => {},
     },
     m('header', 'Trace config'),
     m(
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
deleted file mode 100644
index 3559332..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
+++ /dev/null
@@ -1,682 +0,0 @@
-// Copyright (C) 2022 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 {Attributes} from 'mithril';
-import {assertExists} from '../../base/logging';
-import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
-import {
-  ChromeTargetInfo,
-  RecordingTargetV2,
-  TargetInfo,
-} from './recordingV2/recording_interfaces_v2';
-import {
-  RecordingPageController,
-  RecordingState,
-} from './recordingV2/recording_page_controller';
-import {EXTENSION_NAME, EXTENSION_URL} from './recordingV2/recording_utils';
-import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
-import {PageAttrs} from '../../public/page';
-import {recordConfigStore} from './record_config';
-import {
-  Configurations,
-  loadRecordConfig,
-  maybeGetActiveCss,
-  RECORDING_SECTIONS,
-  uploadRecordingConfig,
-} from './record_page';
-import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './advanced_settings';
-import {AndroidSettings} from './android_settings';
-import {ChromeSettings} from './chrome_settings';
-import {CpuSettings} from './cpu_settings';
-import {EtwSettings} from './etw_settings';
-import {GpuSettings} from './gpu_settings';
-import {LinuxPerfSettings} from './linux_perf_settings';
-import {MemorySettings} from './memory_settings';
-import {PowerSettings} from './power_settings';
-import {RecordingSettings} from './recording_settings';
-import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
-import {showAddNewTargetModal} from './reset_target_modal';
-import {RecordingManager} from './recording_manager';
-import {RecordConfig} from './record_config_types';
-import {App} from '../../public/app';
-import {scheduleFullRedraw} from '../../widgets/raf';
-
-const START_RECORDING_MESSAGE = 'Start Recording';
-
-// TODO(primiano): this is needs to be rewritten, but then i'm going to rewrite
-// the whole record_page_v2 so not worth cleaning up now.
-let controller: RecordingPageController;
-let recordConfigUtils: RecordingConfigUtils;
-
-// Options for displaying a target selection menu.
-export interface TargetSelectionOptions {
-  // css attributes passed to the mithril components which displays the target
-  // selection menu.
-  attributes: Attributes;
-  // Whether the selection should be preceded by a text label.
-  shouldDisplayLabel: boolean;
-}
-
-function isChromeTargetInfo(
-  targetInfo: TargetInfo,
-): targetInfo is ChromeTargetInfo {
-  return ['CHROME', 'CHROME_OS', 'WINDOWS'].includes(targetInfo.targetType);
-}
-
-function RecordHeader(recMgr: RecordingManager) {
-  const platformSelection = RecordingPlatformSelection();
-  const statusLabel = RecordingStatusLabel(recMgr);
-  const buttons = RecordingButton(recMgr.state.recordConfig);
-  const notes = RecordingNotes(recMgr.state.recordConfig);
-  if (!platformSelection && !statusLabel && !buttons && !notes) {
-    // The header should not be displayed when it has no content.
-    return undefined;
-  }
-  return m(
-    '.record-header',
-    m(
-      '.top-part',
-      m('.target-and-status', platformSelection, statusLabel),
-      buttons,
-    ),
-    notes,
-  );
-}
-
-function RecordingPlatformSelection() {
-  // Don't show the platform selector while we are recording a trace.
-  if (controller.getState() >= RecordingState.RECORDING) return undefined;
-
-  return m(
-    '.target',
-    m(
-      '.chip',
-      {onclick: () => showAddNewTargetModal(controller)},
-      m('button', 'Add new recording target'),
-      m('i.material-icons', 'add'),
-    ),
-    targetSelection(),
-  );
-}
-
-export function targetSelection(): m.Vnode | undefined {
-  if (!controller.shouldShowTargetSelection()) {
-    return undefined;
-  }
-
-  const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
-  const targetNames = [];
-  const targetInfo = controller.getTargetInfo();
-  if (!targetInfo) {
-    targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
-  }
-
-  let selectedIndex = 0;
-  for (let i = 0; i < targets.length; i++) {
-    const targetName = targets[i].getInfo().name;
-    targetNames.push(m('option', targetName));
-    if (targetInfo && targetName === targetInfo.name) {
-      selectedIndex = i;
-    }
-  }
-
-  return m(
-    'label',
-    'Target platform:',
-    m(
-      'select',
-      {
-        selectedIndex,
-        onchange: (e: Event) => {
-          controller.onTargetSelection((e.target as HTMLSelectElement).value);
-        },
-        onupdate: (select) => {
-          // Work around mithril bug
-          // (https://github.com/MithrilJS/mithril.js/issues/2107): We may
-          // update the select's options while also changing the
-          // selectedIndex at the same time. The update of selectedIndex
-          // may be applied before the new options are added to the select
-          // element. Because the new selectedIndex may be outside of the
-          // select's options at that time, we have to reselect the
-          // correct index here after any new children were added.
-          (select.dom as HTMLSelectElement).selectedIndex = selectedIndex;
-        },
-      },
-      ...targetNames,
-    ),
-  );
-}
-
-// This will display status messages which are informative, but do not require
-// user action, such as: "Recording in progress for X seconds" in the recording
-// page header.
-function RecordingStatusLabel(recMgr: RecordingManager) {
-  const recordingStatus = recMgr.state.recordingStatus;
-  if (!recordingStatus) return undefined;
-  return m('label', recordingStatus);
-}
-
-function Instructions(recCfg: RecordConfig, cssClass: string) {
-  if (controller.getState() < RecordingState.TARGET_SELECTED) {
-    return undefined;
-  }
-  // We will have a valid target at this step because we checked the state.
-  const targetInfo = assertExists(controller.getTargetInfo());
-
-  return m(
-    `.record-section.instructions${cssClass}`,
-    m('header', 'Recording command'),
-    m(
-      'button.permalinkconfig',
-      {
-        onclick: () => uploadRecordingConfig(recCfg),
-      },
-      'Share recording settings',
-    ),
-    RecordingSnippet(recCfg, targetInfo),
-    BufferUsageProgressBar(),
-    m('.buttons', StopCancelButtons()),
-  );
-}
-
-function BufferUsageProgressBar() {
-  // Show the Buffer Usage bar only after we start recording a trace.
-  if (controller.getState() !== RecordingState.RECORDING) {
-    return undefined;
-  }
-
-  controller.fetchBufferUsage();
-
-  const bufferUsage = controller.getBufferUsagePercentage();
-  // Buffer usage is not available yet on Android.
-  if (bufferUsage === 0) return undefined;
-
-  return m(
-    'label',
-    'Buffer usage: ',
-    m('progress', {max: 100, value: bufferUsage * 100}),
-  );
-}
-
-function RecordingNotes(recCfg: RecordConfig) {
-  if (controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
-    return undefined;
-  }
-  // We will have a valid target at this step because we checked the state.
-  const targetInfo = assertExists(controller.getTargetInfo());
-
-  const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
-  const cmdlineUrl =
-    'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';
-
-  const notes: m.Children = [];
-
-  const msgFeatNotSupported = m(
-    'span',
-    `Some probes are only supported in Perfetto versions running
-      on Android Q+. Therefore, Perfetto will sideload the latest version onto
-      the device.`,
-  );
-
-  const msgPerfettoNotSupported = m(
-    'span',
-    `Perfetto is not supported natively before Android P. Therefore, Perfetto
-       will sideload the latest version onto the device.`,
-  );
-
-  const msgLinux = m(
-    '.note',
-    `Use this `,
-    m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`),
-    ` to get started with tracing on Linux.`,
-  );
-
-  const msgLongTraces = m(
-    '.note',
-    `Recording in long trace mode through the UI is not supported. Please copy
-    the command and `,
-    m(
-      'a',
-      {href: cmdlineUrl, target: '_blank'},
-      `collect the trace using ADB.`,
-    ),
-  );
-
-  if (
-    !recordConfigUtils.fetchLatestRecordCommand(recCfg, targetInfo)
-      .hasDataSources
-  ) {
-    notes.push(
-      m(
-        '.note',
-        "It looks like you didn't add any probes. " +
-          'Please add at least one to get a non-empty trace.',
-      ),
-    );
-  }
-
-  targetFactoryRegistry.listRecordingProblems().map((recordingProblem) => {
-    if (recordingProblem.includes(EXTENSION_URL)) {
-      // Special case for rendering the link to the Chrome extension.
-      const parts = recordingProblem.split(EXTENSION_URL);
-      notes.push(
-        m(
-          '.note',
-          parts[0],
-          m('a', {href: EXTENSION_URL, target: '_blank'}, EXTENSION_NAME),
-          parts[1],
-        ),
-      );
-    }
-  });
-
-  switch (targetInfo.targetType) {
-    case 'LINUX':
-      notes.push(msgLinux);
-      break;
-    case 'ANDROID': {
-      const androidApiLevel = targetInfo.androidApiLevel;
-      if (androidApiLevel === 28) {
-        notes.push(m('.note', msgFeatNotSupported));
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-      } else if (androidApiLevel && androidApiLevel <= 27) {
-        /* eslint-enable */
-        notes.push(m('.note', msgPerfettoNotSupported));
-      }
-      break;
-    }
-    default:
-  }
-
-  if (recCfg.mode === 'LONG_TRACE') {
-    notes.unshift(msgLongTraces);
-  }
-
-  return notes.length > 0 ? m('div', notes) : undefined;
-}
-
-function RecordingSnippet(recCfg: RecordConfig, targetInfo: TargetInfo) {
-  // We don't need commands to start tracing on chrome
-  if (isChromeTargetInfo(targetInfo)) {
-    if (controller.getState() > RecordingState.AUTH_P2) {
-      // If the UI has started tracing, don't display a message guiding the user
-      // to start recording.
-      return undefined;
-    }
-    return m(
-      'div',
-      m(
-        'label',
-        `To trace Chrome from the Perfetto UI you just have to press
-         '${START_RECORDING_MESSAGE}'.`,
-      ),
-    );
-  }
-  return m(CodeSnippet, {text: getRecordCommand(recCfg, targetInfo)});
-}
-
-function getRecordCommand(
-  recCfg: RecordConfig,
-  targetInfo: TargetInfo,
-): string {
-  const recordCommand = recordConfigUtils.fetchLatestRecordCommand(
-    recCfg,
-    targetInfo,
-  );
-
-  const pbBase64 = recordCommand?.configProtoBase64 ?? '';
-  const pbtx = recordCommand?.configProtoText ?? '';
-  let cmd = '';
-  if (
-    targetInfo.targetType === 'ANDROID' &&
-    targetInfo.androidApiLevel === 28
-  ) {
-    cmd += `echo '${pbBase64}' | \n`;
-    cmd += 'base64 --decode | \n';
-    cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
-  } else {
-    cmd +=
-      targetInfo.targetType === 'ANDROID'
-        ? 'adb shell perfetto \\\n'
-        : 'perfetto \\\n';
-    cmd += '  -c - --txt \\\n';
-    cmd += '  -o /data/misc/perfetto-traces/trace \\\n';
-    cmd += '<<EOF\n\n';
-    cmd += pbtx;
-    cmd += '\nEOF\n';
-  }
-  return cmd;
-}
-
-function RecordingButton(recCfg: RecordConfig) {
-  if (
-    controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
-    !controller.canCreateTracingSession()
-  ) {
-    return undefined;
-  }
-
-  // We know we have a target because we checked the state.
-  const targetInfo = assertExists(controller.getTargetInfo());
-  const hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
-    recCfg,
-    targetInfo,
-  ).hasDataSources;
-  if (!hasDataSources) {
-    return undefined;
-  }
-
-  return m(
-    '.button',
-    m(
-      'button',
-      {
-        class: 'selected',
-        onclick: () => controller.onStartRecordingPressed(),
-      },
-      START_RECORDING_MESSAGE,
-    ),
-  );
-}
-
-function StopCancelButtons() {
-  // Show the Stop/Cancel buttons only while we are recording a trace.
-  if (!controller.shouldShowStopCancelButtons()) {
-    return undefined;
-  }
-
-  const stop = m(
-    `button.selected`,
-    {onclick: () => controller.onStop()},
-    'Stop',
-  );
-
-  const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');
-
-  return [stop, cancel];
-}
-
-function recordMenu(routePage: string) {
-  const chromeProbe = m(
-    'a[href="#!/record/chrome"]',
-    m(
-      `li${routePage === 'chrome' ? '.active' : ''}`,
-      m('i.material-icons', 'laptop_chromebook'),
-      m('.title', 'Chrome'),
-      m('.sub', 'Chrome traces'),
-    ),
-  );
-  const cpuProbe = m(
-    'a[href="#!/record/cpu"]',
-    m(
-      `li${routePage === 'cpu' ? '.active' : ''}`,
-      m('i.material-icons', 'subtitles'),
-      m('.title', 'CPU'),
-      m('.sub', 'CPU usage, scheduling, wakeups'),
-    ),
-  );
-  const gpuProbe = m(
-    'a[href="#!/record/gpu"]',
-    m(
-      `li${routePage === 'gpu' ? '.active' : ''}`,
-      m('i.material-icons', 'aspect_ratio'),
-      m('.title', 'GPU'),
-      m('.sub', 'GPU frequency, memory'),
-    ),
-  );
-  const powerProbe = m(
-    'a[href="#!/record/power"]',
-    m(
-      `li${routePage === 'power' ? '.active' : ''}`,
-      m('i.material-icons', 'battery_charging_full'),
-      m('.title', 'Power'),
-      m('.sub', 'Battery and other energy counters'),
-    ),
-  );
-  const memoryProbe = m(
-    'a[href="#!/record/memory"]',
-    m(
-      `li${routePage === 'memory' ? '.active' : ''}`,
-      m('i.material-icons', 'memory'),
-      m('.title', 'Memory'),
-      m('.sub', 'Physical mem, VM, LMK'),
-    ),
-  );
-  const androidProbe = m(
-    'a[href="#!/record/android"]',
-    m(
-      `li${routePage === 'android' ? '.active' : ''}`,
-      m('i.material-icons', 'android'),
-      m('.title', 'Android apps & svcs'),
-      m('.sub', 'atrace and logcat'),
-    ),
-  );
-  const advancedProbe = m(
-    'a[href="#!/record/advanced"]',
-    m(
-      `li${routePage === 'advanced' ? '.active' : ''}`,
-      m('i.material-icons', 'settings'),
-      m('.title', 'Advanced settings'),
-      m('.sub', 'Complicated stuff for wizards'),
-    ),
-  );
-  const tracePerfProbe = m(
-    'a[href="#!/record/tracePerf"]',
-    m(
-      `li${routePage === 'tracePerf' ? '.active' : ''}`,
-      m('i.material-icons', 'full_stacked_bar_chart'),
-      m('.title', 'Stack Samples'),
-      m('.sub', 'Lightweight stack polling'),
-    ),
-  );
-  const etwProbe = m(
-    'a[href="#!/record/etw"]',
-    m(
-      `li${routePage === 'etw' ? '.active' : ''}`,
-      m('i.material-icons', 'subtitles'),
-      m('.title', 'ETW Tracing Config'),
-      m('.sub', 'Context switch, Thread state'),
-    ),
-  );
-
-  // We only display the probes when we have a valid target, so it's not
-  // possible for the target to be undefined here.
-  const targetType = assertExists(controller.getTargetInfo()).targetType;
-  const probes = [];
-  if (targetType === 'LINUX') {
-    probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
-  } else if (targetType === 'WINDOWS') {
-    probes.push(chromeProbe, etwProbe);
-  } else if (targetType === 'CHROME') {
-    probes.push(chromeProbe);
-  } else {
-    probes.push(
-      cpuProbe,
-      gpuProbe,
-      powerProbe,
-      memoryProbe,
-      androidProbe,
-      chromeProbe,
-      tracePerfProbe,
-      advancedProbe,
-    );
-  }
-
-  return m(
-    '.record-menu',
-    {
-      class:
-        controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
-          ? 'disabled'
-          : '',
-      onclick: () => scheduleFullRedraw(),
-    },
-    m('header', 'Trace config'),
-    m(
-      'ul',
-      m(
-        'a[href="#!/record/buffers"]',
-        m(
-          `li${routePage === 'buffers' ? '.active' : ''}`,
-          m('i.material-icons', 'tune'),
-          m('.title', 'Recording settings'),
-          m('.sub', 'Buffer mode, size and duration'),
-        ),
-      ),
-      m(
-        'a[href="#!/record/instructions"]',
-        m(
-          `li${routePage === 'instructions' ? '.active' : ''}`,
-          m('i.material-icons-filled.rec', 'fiber_manual_record'),
-          m('.title', 'Recording command'),
-          m('.sub', 'Manually record trace'),
-        ),
-      ),
-      m(
-        'a[href="#!/record/config"]',
-        {
-          onclick: () => {
-            recordConfigStore.reloadFromLocalStorage();
-          },
-        },
-        m(
-          `li${routePage === 'config' ? '.active' : ''}`,
-          m('i.material-icons', 'save'),
-          m('.title', 'Saved configs'),
-          m('.sub', 'Manage local configs'),
-        ),
-      ),
-    ),
-    m('header', 'Probes'),
-    m('ul', probes),
-  );
-}
-
-function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
-  const recCfg = recMgr.state.recordConfig;
-  const components: m.Children[] = [RecordHeader(recMgr)];
-  if (controller.getState() === RecordingState.NO_TARGET) {
-    components.push(m('.full-centered', 'Please connect a valid target.'));
-    return m('.record-container', components);
-  } else if (controller.getState() <= RecordingState.ASK_TO_FORCE_P1) {
-    components.push(
-      m(
-        '.full-centered',
-        'Can not access the device without resetting the ' +
-          `connection. Please refresh the page, then click ` +
-          `'${FORCE_RESET_MESSAGE}.'`,
-      ),
-    );
-    return m('.record-container', components);
-  } else if (controller.getState() === RecordingState.AUTH_P1) {
-    components.push(
-      m('.full-centered', 'Please allow USB debugging on the device.'),
-    );
-    return m('.record-container', components);
-  } else if (
-    controller.getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
-  ) {
-    components.push(
-      m('.full-centered', 'Waiting for the trace to be collected.'),
-    );
-    return m('.record-container', components);
-  }
-
-  const pages: m.Children = [];
-  // we need to remove the `/` character from the route
-  let routePage = subpage ? subpage.substr(1) : '';
-  if (!RECORDING_SECTIONS.includes(routePage)) {
-    routePage = 'buffers';
-  }
-  pages.push(recordMenu(routePage));
-
-  pages.push(
-    m(RecordingSettings, {
-      dataSources: [],
-      cssClass: maybeGetActiveCss(routePage, 'buffers'),
-      recState: recMgr.state,
-    }),
-  );
-  pages.push(
-    Instructions(recCfg, maybeGetActiveCss(routePage, 'instructions')),
-  );
-  pages.push(Configurations(recMgr, maybeGetActiveCss(routePage, 'config')));
-
-  const settingsSections = new Map([
-    ['cpu', CpuSettings],
-    ['gpu', GpuSettings],
-    ['power', PowerSettings],
-    ['memory', MemorySettings],
-    ['android', AndroidSettings],
-    ['chrome', ChromeSettings],
-    ['tracePerf', LinuxPerfSettings],
-    ['advanced', AdvancedSettings],
-    ['etw', EtwSettings],
-  ]);
-  for (const [section, component] of settingsSections.entries()) {
-    pages.push(
-      m(component, {
-        dataSources: controller.getTargetInfo()?.dataSources || [],
-        cssClass: maybeGetActiveCss(routePage, section),
-        recState: recMgr.state,
-      }),
-    );
-  }
-
-  components.push(m('.record-container-content', pages));
-  return m('.record-container', components);
-}
-
-export interface RecordPageV2Attrs extends PageAttrs {
-  app: App;
-  recCtl: RecordingPageController;
-  recMgr: RecordingManager;
-}
-
-export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
-  private lastSubpage: string | undefined = undefined;
-
-  constructor({attrs}: m.CVnode<RecordPageV2Attrs>) {
-    controller ??= attrs.recCtl;
-    recordConfigUtils ??= new RecordingConfigUtils();
-  }
-
-  oninit({attrs}: m.CVnode<RecordPageV2Attrs>) {
-    this.lastSubpage = attrs.subpage;
-    if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
-      const hash = attrs.subpage.substring(7);
-      loadRecordConfig(attrs.recMgr, hash);
-      attrs.app.navigate('#!/record/instructions');
-    }
-  }
-
-  view({attrs}: m.CVnode<RecordPageV2Attrs>) {
-    if (attrs.subpage !== this.lastSubpage) {
-      this.lastSubpage = attrs.subpage;
-      // TODO(primiano): this is a hack necesasry to retrigger the generation of
-      // the record cmdline. Refactor this code once record v1 vs v2 is gone.
-      attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
-    }
-
-    return m(
-      '.record-page',
-      controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
-        ? m('.hider')
-        : [],
-      getRecordContainer(attrs.recMgr, attrs.subpage),
-    );
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
index 325237b..8ffdd8e 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -17,7 +17,6 @@
 import {assertExists} from '../../base/logging';
 import {RecordConfig} from './record_config_types';
 import {assetSrc} from '../../base/assets';
-import {scheduleFullRedraw} from '../../widgets/raf';
 
 export declare type Setter<T> = (cfg: RecordConfig, val: T) => void;
 export declare type Getter<T> = (cfg: RecordConfig) => T;
@@ -63,7 +62,6 @@
   view({attrs, children}: m.CVnode<ProbeAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -130,7 +128,6 @@
   view({attrs}: m.CVnode<ToggleAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -175,7 +172,6 @@
 export class Slider implements m.ClassComponent<SliderAttrs> {
   onValueChange(attrs: SliderAttrs, newVal: number) {
     attrs.set(attrs.recCfg, newVal);
-    scheduleFullRedraw();
   }
 
   onTimeValueChange(attrs: SliderAttrs, hms: string) {
@@ -276,7 +272,6 @@
       selKeys.push(item.value);
     }
     attrs.set(attrs.recCfg, selKeys);
-    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<DropdownAttrs>) {
@@ -326,7 +321,6 @@
 export class Textarea implements m.ClassComponent<TextareaAttrs> {
   onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
     attrs.set(attrs.recCfg, dom.value);
-    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<TextareaAttrs>) {
@@ -400,7 +394,6 @@
     if (!enabled && index !== -1) {
       values.splice(index, 1);
     }
-    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
deleted file mode 100644
index 33e0dc1..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) 2022 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 {defer} from '../../../base/deferred';
-import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
-import {AdbFileHandler} from './adb_file_handler';
-import {
-  AdbConnection,
-  ByteStream,
-  OnDisconnectCallback,
-  OnMessageCallback,
-} from './recording_interfaces_v2';
-import {utf8Decode} from '../../../base/string_utils';
-
-export abstract class AdbConnectionImpl implements AdbConnection {
-  // onStatus and onDisconnect are set to callbacks passed from the caller.
-  // This happens for instance in the AndroidWebusbTarget, which instantiates
-  // them with callbacks passed from the UI.
-  onStatus: OnMessageCallback = () => {};
-  onDisconnect: OnDisconnectCallback = (_) => {};
-
-  // Starts a shell command, and returns a promise resolved when the command
-  // completes.
-  async shellAndWaitCompletion(cmd: string): Promise<void> {
-    const adbStream = await this.shell(cmd);
-    const onStreamingEnded = defer<void>();
-
-    // We wait for the stream to be closed by the device, which happens
-    // after the shell command is successfully received.
-    adbStream.addOnStreamCloseCallback(() => {
-      onStreamingEnded.resolve();
-    });
-    return onStreamingEnded;
-  }
-
-  // Starts a shell command, then gathers all its output and returns it as
-  // a string.
-  async shellAndGetOutput(cmd: string): Promise<string> {
-    const adbStream = await this.shell(cmd);
-    const commandOutput = new ArrayBufferBuilder();
-    const onStreamingEnded = defer<string>();
-
-    adbStream.addOnStreamDataCallback((data: Uint8Array) => {
-      commandOutput.append(data);
-    });
-    adbStream.addOnStreamCloseCallback(() => {
-      onStreamingEnded.resolve(utf8Decode(commandOutput.toArrayBuffer()));
-    });
-    return onStreamingEnded;
-  }
-
-  async push(binary: Uint8Array, path: string): Promise<void> {
-    const byteStream = await this.openStream('sync:');
-    await new AdbFileHandler(byteStream).pushBinary(binary, path);
-    // We need to wait until the bytestream is closed. Otherwise, we can have a
-    // race condition:
-    // If this is the last stream, it will try to disconnect the device. In the
-    // meantime, the caller might create another stream which will try to open
-    // the device.
-    await byteStream.closeAndWaitForTeardown();
-  }
-
-  abstract shell(cmd: string): Promise<ByteStream>;
-
-  abstract canConnectWithoutContention(): Promise<boolean>;
-
-  abstract connectSocket(path: string): Promise<ByteStream>;
-
-  abstract disconnect(disconnectMessage?: string): Promise<void>;
-
-  protected abstract openStream(destination: string): Promise<ByteStream>;
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
deleted file mode 100644
index 9c9d139..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-// Copyright (C) 2022 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 {defer, Deferred} from '../../../base/deferred';
-import {utf8Decode} from '../../../base/string_utils';
-import {AdbConnectionImpl} from './adb_connection_impl';
-import {RecordingError} from './recording_error_handling';
-import {
-  ByteStream,
-  OnDisconnectCallback,
-  OnStreamCloseCallback,
-  OnStreamDataCallback,
-} from './recording_interfaces_v2';
-import {
-  ALLOW_USB_DEBUGGING,
-  buildAbdWebsocketCommand,
-  WEBSOCKET_UNABLE_TO_CONNECT,
-} from './recording_utils';
-
-export class AdbConnectionOverWebsocket extends AdbConnectionImpl {
-  private streams = new Set<AdbOverWebsocketStream>();
-
-  onDisconnect: OnDisconnectCallback = (_) => {};
-
-  constructor(
-    private deviceSerialNumber: string,
-    private websocketUrl: string,
-  ) {
-    super();
-  }
-
-  shell(cmd: string): Promise<AdbOverWebsocketStream> {
-    return this.openStream('shell:' + cmd);
-  }
-
-  connectSocket(path: string): Promise<AdbOverWebsocketStream> {
-    return this.openStream(path);
-  }
-
-  protected async openStream(
-    destination: string,
-  ): Promise<AdbOverWebsocketStream> {
-    return AdbOverWebsocketStream.create(
-      this.websocketUrl,
-      destination,
-      this.deviceSerialNumber,
-      this.closeStream.bind(this),
-    );
-  }
-
-  // The disconnection for AdbConnectionOverWebsocket is synchronous, but this
-  // method is async to have a common interface with other types of connections
-  // which are async.
-  async disconnect(disconnectMessage?: string): Promise<void> {
-    for (const stream of this.streams) {
-      stream.close();
-    }
-    this.onDisconnect(disconnectMessage);
-  }
-
-  closeStream(stream: AdbOverWebsocketStream): void {
-    if (this.streams.has(stream)) {
-      this.streams.delete(stream);
-    }
-  }
-
-  // There will be no contention for the websocket connection, because it will
-  // communicate with the 'adb server' running on the computer which opened
-  // Perfetto.
-  canConnectWithoutContention(): Promise<boolean> {
-    return Promise.resolve(true);
-  }
-}
-
-// An AdbOverWebsocketStream instantiates a websocket connection to the device.
-// It exposes an API to write commands to this websocket and read its output.
-export class AdbOverWebsocketStream implements ByteStream {
-  private websocket: WebSocket;
-
-  // commandSentSignal gets resolved if we successfully connect to the device
-  // and send the command this socket wraps. commandSentSignal gets rejected if
-  // we fail to connect to the device.
-  private commandSentSignal = defer<AdbOverWebsocketStream>();
-
-  // We store a promise for each messge while the message is processed.
-  // This way, if the websocket server closes the connection, we first process
-  // all previously received messages and only afterwards disconnect.
-  // An application is when the stream wraps a shell command. The websocket
-  // server will reply and then immediately disconnect.
-  private messageProcessedSignals: Set<Deferred<void>> = new Set();
-
-  private _isConnected = false;
-  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
-  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
-
-  private constructor(
-    websocketUrl: string,
-    destination: string,
-    deviceSerialNumber: string,
-    private removeFromConnection: (stream: AdbOverWebsocketStream) => void,
-  ) {
-    this.websocket = new WebSocket(websocketUrl);
-
-    this.websocket.onopen = this.onOpen.bind(this, deviceSerialNumber);
-    this.websocket.onmessage = this.onMessage.bind(this, destination);
-    // The websocket may be closed by the websocket server. This happens
-    // for instance when we get the full result of a shell command.
-    this.websocket.onclose = this.onClose.bind(this);
-  }
-
-  addOnStreamDataCallback(onStreamData: OnStreamDataCallback) {
-    this.onStreamDataCallbacks.push(onStreamData);
-  }
-
-  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback) {
-    this.onStreamCloseCallbacks.push(onStreamClose);
-  }
-
-  // Used by the connection object to signal newly received data, not exposed
-  // in the interface.
-  signalStreamData(data: Uint8Array): void {
-    for (const onStreamData of this.onStreamDataCallbacks) {
-      onStreamData(data);
-    }
-  }
-
-  // Used by the connection object to signal the stream is closed, not exposed
-  // in the interface.
-  signalStreamClosed(): void {
-    for (const onStreamClose of this.onStreamCloseCallbacks) {
-      onStreamClose();
-    }
-    this.onStreamDataCallbacks = [];
-    this.onStreamCloseCallbacks = [];
-  }
-
-  // We close the websocket and notify the AdbConnection to remove this stream.
-  close(): void {
-    // If the websocket connection is still open (ie. the close did not
-    // originate from the server), we close the websocket connection.
-    if (this.websocket.readyState === this.websocket.OPEN) {
-      this.websocket.close();
-      // We remove the 'onclose' callback so the 'close' method doesn't get
-      // executed twice.
-      this.websocket.onclose = null;
-    }
-    this._isConnected = false;
-    this.removeFromConnection(this);
-    this.signalStreamClosed();
-  }
-
-  // For websocket, the teardown happens synchronously.
-  async closeAndWaitForTeardown(): Promise<void> {
-    this.close();
-  }
-
-  write(msg: string | Uint8Array): void {
-    this.websocket.send(msg);
-  }
-
-  isConnected(): boolean {
-    return this._isConnected;
-  }
-
-  private async onOpen(deviceSerialNumber: string): Promise<void> {
-    this.websocket.send(
-      buildAbdWebsocketCommand(`host:transport:${deviceSerialNumber}`),
-    );
-  }
-
-  private async onMessage(
-    destination: string,
-    evt: MessageEvent,
-  ): Promise<void> {
-    const messageProcessed = defer<void>();
-    this.messageProcessedSignals.add(messageProcessed);
-    try {
-      if (!this._isConnected) {
-        const txt = (await evt.data.text()) as string;
-        const prefix = txt.substring(0, 4);
-        if (prefix === 'OKAY') {
-          this._isConnected = true;
-          this.websocket.send(buildAbdWebsocketCommand(destination));
-          this.commandSentSignal.resolve(this);
-        } else if (prefix === 'FAIL' && txt.includes('device unauthorized')) {
-          this.commandSentSignal.reject(
-            new RecordingError(ALLOW_USB_DEBUGGING),
-          );
-          this.close();
-        } else {
-          this.commandSentSignal.reject(
-            new RecordingError(WEBSOCKET_UNABLE_TO_CONNECT),
-          );
-          this.close();
-        }
-      } else {
-        // Upon a successful connection we first receive an 'OKAY' message.
-        // After that, we receive messages with traced binary payloads.
-        const arrayBufferResponse = await evt.data.arrayBuffer();
-        if (utf8Decode(arrayBufferResponse) !== 'OKAY') {
-          this.signalStreamData(new Uint8Array(arrayBufferResponse));
-        }
-      }
-      messageProcessed.resolve();
-    } finally {
-      this.messageProcessedSignals.delete(messageProcessed);
-    }
-  }
-
-  private async onClose(): Promise<void> {
-    // Wait for all messages to be processed before closing the connection.
-    await Promise.allSettled(this.messageProcessedSignals);
-    this.close();
-  }
-
-  static create(
-    websocketUrl: string,
-    destination: string,
-    deviceSerialNumber: string,
-    removeFromConnection: (stream: AdbOverWebsocketStream) => void,
-  ): Promise<AdbOverWebsocketStream> {
-    return new AdbOverWebsocketStream(
-      websocketUrl,
-      destination,
-      deviceSerialNumber,
-      removeFromConnection,
-    ).commandSentSignal;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
deleted file mode 100644
index 715d366..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
+++ /dev/null
@@ -1,674 +0,0 @@
-// Copyright (C) 2022 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 {defer, Deferred} from '../../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
-import {isString} from '../../../base/object_utils';
-import {utf8Decode, utf8Encode} from '../../../base/string_utils';
-import {CmdType} from '../adb_interfaces';
-import {AdbConnectionImpl} from './adb_connection_impl';
-import {AdbKeyManager, maybeStoreKey} from './auth/adb_key_manager';
-import {RecordingError, wrapRecordingError} from './recording_error_handling';
-import {
-  ByteStream,
-  OnStreamCloseCallback,
-  OnStreamDataCallback,
-} from './recording_interfaces_v2';
-import {ALLOW_USB_DEBUGGING, findInterfaceAndEndpoint} from './recording_utils';
-
-export const VERSION_WITH_CHECKSUM = 0x01000000;
-export const VERSION_NO_CHECKSUM = 0x01000001;
-export const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
-
-export enum AdbState {
-  DISCONNECTED = 0,
-  // Authentication steps, see AdbConnectionOverWebUsb's handleAuthentication().
-  AUTH_STARTED = 1,
-  AUTH_WITH_PRIVATE = 2,
-  AUTH_WITH_PUBLIC = 3,
-
-  CONNECTED = 4,
-}
-
-enum AuthCmd {
-  TOKEN = 1,
-  SIGNATURE = 2,
-  RSAPUBLICKEY = 3,
-}
-
-function generateChecksum(data: Uint8Array): number {
-  let res = 0;
-  for (let i = 0; i < data.byteLength; i++) res += data[i];
-  return res & 0xffffffff;
-}
-
-// Message to be written to the adb connection. Contains the message itself
-// and the corresponding stream identifier.
-interface WriteQueueElement {
-  message: Uint8Array;
-  localStreamId: number;
-}
-
-export class AdbConnectionOverWebusb extends AdbConnectionImpl {
-  private state: AdbState = AdbState.DISCONNECTED;
-  private connectingStreams = new Map<number, Deferred<AdbOverWebusbStream>>();
-  private streams = new Set<AdbOverWebusbStream>();
-  private maxPayload = DEFAULT_MAX_PAYLOAD_BYTES;
-  private writeInProgress = false;
-  private writeQueue: WriteQueueElement[] = [];
-
-  // Devices after Dec 2017 don't use checksum. This will be auto-detected
-  // during the connection.
-  private useChecksum = true;
-
-  private lastStreamId = 0;
-  private usbInterfaceNumber?: number;
-  private usbReadEndpoint = -1;
-  private usbWriteEpEndpoint = -1;
-  private isUsbReceiveLoopRunning = false;
-
-  private pendingConnPromises: Array<Deferred<void>> = [];
-
-  // We use a key pair for authenticating with the device, which we do in
-  // two ways:
-  // - Firstly, signing with the private key.
-  // - Secondly, sending over the public key (at which point the device asks the
-  //   user for permissions).
-  // Once we've sent the public key, for future recordings we only need to
-  // sign with the private key, so the user doesn't need to give permissions
-  // again.
-  constructor(
-    private device: USBDevice,
-    private keyManager: AdbKeyManager,
-  ) {
-    super();
-  }
-
-  shell(cmd: string): Promise<AdbOverWebusbStream> {
-    return this.openStream('shell:' + cmd);
-  }
-
-  connectSocket(path: string): Promise<AdbOverWebusbStream> {
-    return this.openStream(path);
-  }
-
-  async canConnectWithoutContention(): Promise<boolean> {
-    await this.device.open();
-    const usbInterfaceNumber = await this.setupUsbInterface();
-    try {
-      await this.device.claimInterface(usbInterfaceNumber);
-      await this.device.releaseInterface(usbInterfaceNumber);
-      return true;
-    } catch (e) {
-      return false;
-    }
-  }
-
-  protected async openStream(
-    destination: string,
-  ): Promise<AdbOverWebusbStream> {
-    const streamId = ++this.lastStreamId;
-    const connectingStream = defer<AdbOverWebusbStream>();
-    this.connectingStreams.set(streamId, connectingStream);
-    // We create the stream before trying to establish the connection, so
-    // that if we fail to connect, we will reject the connecting stream.
-    await this.ensureConnectionEstablished();
-    await this.sendMessage('OPEN', streamId, 0, destination);
-    return connectingStream;
-  }
-
-  private async ensureConnectionEstablished(): Promise<void> {
-    if (this.state === AdbState.CONNECTED) {
-      return;
-    }
-
-    if (this.state === AdbState.DISCONNECTED) {
-      await this.device.open();
-      if (!(await this.canConnectWithoutContention())) {
-        await this.device.reset();
-      }
-      const usbInterfaceNumber = await this.setupUsbInterface();
-      await this.device.claimInterface(usbInterfaceNumber);
-    }
-
-    await this.startAdbAuth();
-    if (!this.isUsbReceiveLoopRunning) {
-      this.usbReceiveLoop();
-    }
-    const connPromise = defer<void>();
-    this.pendingConnPromises.push(connPromise);
-    await connPromise;
-  }
-
-  private async setupUsbInterface(): Promise<number> {
-    const interfaceAndEndpoint = findInterfaceAndEndpoint(this.device);
-    // `findInterfaceAndEndpoint` will always return a non-null value because
-    // we check for this in 'android_webusb_target_factory'. If no interface and
-    // endpoints are found, we do not create a target, so we can not connect to
-    // it, so we will never reach this logic.
-    const {configurationValue, usbInterfaceNumber, endpoints} =
-      assertExists(interfaceAndEndpoint);
-    this.usbInterfaceNumber = usbInterfaceNumber;
-    this.usbReadEndpoint = this.findEndpointNumber(endpoints, 'in');
-    this.usbWriteEpEndpoint = this.findEndpointNumber(endpoints, 'out');
-    assertTrue(this.usbReadEndpoint >= 0 && this.usbWriteEpEndpoint >= 0);
-    await this.device.selectConfiguration(configurationValue);
-    return usbInterfaceNumber;
-  }
-
-  async streamClose(stream: AdbOverWebusbStream): Promise<void> {
-    const otherStreamsQueue = this.writeQueue.filter(
-      (queueElement) => queueElement.localStreamId !== stream.localStreamId,
-    );
-    const droppedPacketCount =
-      this.writeQueue.length - otherStreamsQueue.length;
-    if (droppedPacketCount > 0) {
-      console.debug(
-        `Dropping ${droppedPacketCount} queued messages due to stream closing.`,
-      );
-      this.writeQueue = otherStreamsQueue;
-    }
-
-    this.streams.delete(stream);
-    if (this.streams.size === 0) {
-      // We disconnect BEFORE calling `signalStreamClosed`. Otherwise, there can
-      // be a race condition:
-      // Stream A: streamA.onStreamClose
-      // Stream B: device.open
-      // Stream A: device.releaseInterface
-      // Stream B: device.transferOut -> CRASH
-      await this.disconnect();
-    }
-    stream.signalStreamClosed();
-  }
-
-  streamWrite(msg: string | Uint8Array, stream: AdbOverWebusbStream): void {
-    const raw = isString(msg) ? utf8Encode(msg) : msg;
-    if (this.writeInProgress) {
-      this.writeQueue.push({message: raw, localStreamId: stream.localStreamId});
-      return;
-    }
-    this.writeInProgress = true;
-    this.sendMessage('WRTE', stream.localStreamId, stream.remoteStreamId, raw);
-  }
-
-  // We disconnect in 2 cases:
-  // 1. When we close the last stream of the connection. This is to prevent the
-  // browser holding onto the USB interface after having finished a trace
-  // recording, which would make it impossible to use "adb shell" from the same
-  // machine until the browser is closed.
-  // 2. When we get a USB disconnect event. This happens for instance when the
-  // device is unplugged.
-  async disconnect(disconnectMessage?: string): Promise<void> {
-    if (this.state === AdbState.DISCONNECTED) {
-      return;
-    }
-    // Clear the resources in a synchronous method, because this can be used
-    // for error handling callbacks as well.
-    this.reachDisconnectState(disconnectMessage);
-
-    // We have already disconnected so there is no need to pass a callback
-    // which clears resources or notifies the user into 'wrapRecordingError'.
-    await wrapRecordingError(
-      this.device.releaseInterface(assertExists(this.usbInterfaceNumber)),
-      () => {},
-    );
-    this.usbInterfaceNumber = undefined;
-  }
-
-  // This is a synchronous method which clears all resources.
-  // It can be used as a callback for error handling.
-  reachDisconnectState(disconnectMessage?: string): void {
-    // We need to delete the streams BEFORE checking the Adb state because:
-    //
-    // We create streams before changing the Adb state from DISCONNECTED.
-    // In case we can not claim the device, we will create a stream, but fail
-    // to connect to the WebUSB device so the state will remain DISCONNECTED.
-    const streamsToDelete = this.connectingStreams.entries();
-    // Clear the streams before rejecting so we are not caught in a loop of
-    // handling promise rejections.
-    this.connectingStreams.clear();
-    for (const [id, stream] of streamsToDelete) {
-      stream.reject(
-        `Failed to open stream with id ${id} because adb was disconnected.`,
-      );
-    }
-
-    if (this.state === AdbState.DISCONNECTED) {
-      return;
-    }
-
-    this.state = AdbState.DISCONNECTED;
-    this.writeInProgress = false;
-
-    this.writeQueue = [];
-
-    this.streams.forEach((stream) => stream.close());
-    this.onDisconnect(disconnectMessage);
-  }
-
-  private async startAdbAuth(): Promise<void> {
-    const VERSION = this.useChecksum
-      ? VERSION_WITH_CHECKSUM
-      : VERSION_NO_CHECKSUM;
-    this.state = AdbState.AUTH_STARTED;
-    await this.sendMessage('CNXN', VERSION, this.maxPayload, 'host:1:UsbADB');
-  }
-
-  private findEndpointNumber(
-    endpoints: USBEndpoint[],
-    direction: 'out' | 'in',
-    type = 'bulk',
-  ): number {
-    const ep = endpoints.find(
-      (ep) => ep.type === type && ep.direction === direction,
-    );
-
-    if (ep) return ep.endpointNumber;
-
-    throw new RecordingError(`Cannot find ${direction} endpoint`);
-  }
-
-  private async usbReceiveLoop(): Promise<void> {
-    assertFalse(this.isUsbReceiveLoopRunning);
-    this.isUsbReceiveLoopRunning = true;
-    for (; this.state !== AdbState.DISCONNECTED; ) {
-      const res = await this.wrapUsb(
-        this.device.transferIn(this.usbReadEndpoint, ADB_MSG_SIZE),
-      );
-      if (!res) {
-        this.isUsbReceiveLoopRunning = false;
-        return;
-      }
-      if (res.status !== 'ok') {
-        // Log and ignore messages with invalid status. These can occur
-        // when the device is connected/disconnected repeatedly.
-        console.error(
-          `Received message with unexpected status '${res.status}'`,
-        );
-        continue;
-      }
-
-      const msg = AdbMsg.decodeHeader(res.data!);
-      if (msg.dataLen > 0) {
-        const resp = await this.wrapUsb(
-          this.device.transferIn(this.usbReadEndpoint, msg.dataLen),
-        );
-        if (!resp) {
-          this.isUsbReceiveLoopRunning = false;
-          return;
-        }
-        msg.data = new Uint8Array(
-          resp.data!.buffer,
-          resp.data!.byteOffset,
-          resp.data!.byteLength,
-        );
-      }
-
-      if (this.useChecksum && generateChecksum(msg.data) !== msg.dataChecksum) {
-        // We ignore messages with an invalid checksum. These sometimes appear
-        // when the page is re-loaded in a middle of a recording.
-        continue;
-      }
-      // The server can still send messages streams for previous streams.
-      // This happens for instance if we record, reload the recording page and
-      // then record again. We can also receive a 'WRTE' or 'OKAY' after
-      // we have sent a 'CLSE' and marked the state as disconnected.
-      if (
-        (msg.cmd === 'CLSE' || msg.cmd === 'WRTE') &&
-        !this.getStreamForLocalStreamId(msg.arg1)
-      ) {
-        continue;
-      } else if (
-        msg.cmd === 'OKAY' &&
-        !this.connectingStreams.has(msg.arg1) &&
-        !this.getStreamForLocalStreamId(msg.arg1)
-      ) {
-        continue;
-      } else if (
-        msg.cmd === 'AUTH' &&
-        msg.arg0 === AuthCmd.TOKEN &&
-        this.state === AdbState.AUTH_WITH_PUBLIC
-      ) {
-        // If we start a recording but fail because of a faulty physical
-        // connection to the device, when we start a new recording, we will
-        // received multiple AUTH tokens, of which we should ignore all but
-        // one.
-        continue;
-      }
-
-      // handle the ADB message from the device
-      if (msg.cmd === 'CLSE') {
-        assertExists(this.getStreamForLocalStreamId(msg.arg1)).close();
-      } else if (msg.cmd === 'AUTH' && msg.arg0 === AuthCmd.TOKEN) {
-        const key = await this.keyManager.getKey();
-        if (this.state === AdbState.AUTH_STARTED) {
-          // During this step, we send back the token received signed with our
-          // private key. If the device has previously received our public key,
-          // the dialog asking for user confirmation will not be displayed on
-          // the device.
-          this.state = AdbState.AUTH_WITH_PRIVATE;
-          await this.sendMessage(
-            'AUTH',
-            AuthCmd.SIGNATURE,
-            0,
-            key.sign(msg.data),
-          );
-        } else {
-          // If our signature with the private key is not accepted by the
-          // device, we generate a new keypair and send the public key.
-          this.state = AdbState.AUTH_WITH_PUBLIC;
-          await this.sendMessage(
-            'AUTH',
-            AuthCmd.RSAPUBLICKEY,
-            0,
-            key.getPublicKey() + '\0',
-          );
-          this.onStatus(ALLOW_USB_DEBUGGING);
-          await maybeStoreKey(key);
-        }
-      } else if (msg.cmd === 'CNXN') {
-        assertTrue(
-          [AdbState.AUTH_WITH_PRIVATE, AdbState.AUTH_WITH_PUBLIC].includes(
-            this.state,
-          ),
-        );
-        this.state = AdbState.CONNECTED;
-        this.maxPayload = msg.arg1;
-
-        const deviceVersion = msg.arg0;
-
-        if (
-          ![VERSION_WITH_CHECKSUM, VERSION_NO_CHECKSUM].includes(deviceVersion)
-        ) {
-          throw new RecordingError(`Version ${msg.arg0} not supported.`);
-        }
-        this.useChecksum = deviceVersion === VERSION_WITH_CHECKSUM;
-        this.state = AdbState.CONNECTED;
-
-        // This will resolve the promises awaited by
-        // "ensureConnectionEstablished".
-        this.pendingConnPromises.forEach((connPromise) =>
-          connPromise.resolve(),
-        );
-        this.pendingConnPromises = [];
-      } else if (msg.cmd === 'OKAY') {
-        if (this.connectingStreams.has(msg.arg1)) {
-          const connectingStream = assertExists(
-            this.connectingStreams.get(msg.arg1),
-          );
-          const stream = new AdbOverWebusbStream(this, msg.arg1, msg.arg0);
-          this.streams.add(stream);
-          this.connectingStreams.delete(msg.arg1);
-          connectingStream.resolve(stream);
-        } else {
-          assertTrue(this.writeInProgress);
-          this.writeInProgress = false;
-          for (; this.writeQueue.length; ) {
-            // We go through the queued writes and choose the first one
-            // corresponding to a stream that's still active.
-            const queuedElement = assertExists(this.writeQueue.shift());
-            const queuedStream = this.getStreamForLocalStreamId(
-              queuedElement.localStreamId,
-            );
-            if (queuedStream) {
-              queuedStream.write(queuedElement.message);
-              break;
-            }
-          }
-        }
-      } else if (msg.cmd === 'WRTE') {
-        const stream = assertExists(this.getStreamForLocalStreamId(msg.arg1));
-        await this.sendMessage(
-          'OKAY',
-          stream.localStreamId,
-          stream.remoteStreamId,
-        );
-        stream.signalStreamData(msg.data);
-      } else {
-        this.isUsbReceiveLoopRunning = false;
-        throw new RecordingError(
-          `Unexpected message ${msg} in state ${this.state}`,
-        );
-      }
-    }
-    this.isUsbReceiveLoopRunning = false;
-  }
-
-  private getStreamForLocalStreamId(
-    localStreamId: number,
-  ): AdbOverWebusbStream | undefined {
-    for (const stream of this.streams) {
-      if (stream.localStreamId === localStreamId) {
-        return stream;
-      }
-    }
-    return undefined;
-  }
-
-  //  The header and the message data must be sent consecutively. Using 2 awaits
-  //  Another message can interleave after the first header has been sent,
-  //  resulting in something like [header1] [header2] [data1] [data2];
-  //  In this way we are waiting both promises to be resolved before continuing.
-  private async sendMessage(
-    cmd: CmdType,
-    arg0: number,
-    arg1: number,
-    data?: Uint8Array | string,
-  ): Promise<void> {
-    const msg = AdbMsg.create({
-      cmd,
-      arg0,
-      arg1,
-      data,
-      useChecksum: this.useChecksum,
-    });
-
-    const msgHeader = msg.encodeHeader();
-    const msgData = msg.data;
-    assertTrue(
-      msgHeader.length <= this.maxPayload && msgData.length <= this.maxPayload,
-    );
-
-    const sendPromises = [
-      this.wrapUsb(
-        this.device.transferOut(this.usbWriteEpEndpoint, msgHeader.buffer),
-      ),
-    ];
-    if (msg.data.length > 0) {
-      sendPromises.push(
-        this.wrapUsb(
-          this.device.transferOut(this.usbWriteEpEndpoint, msgData.buffer),
-        ),
-      );
-    }
-    await Promise.all(sendPromises);
-  }
-
-  private wrapUsb<T>(promise: Promise<T>): Promise<T | undefined> {
-    return wrapRecordingError(promise, this.reachDisconnectState.bind(this));
-  }
-}
-
-// An AdbOverWebusbStream is instantiated after the creation of a socket to the
-// device. Thanks to this, we can send commands and receive their output.
-// Messages are received in the main adb class, and are forwarded to an instance
-// of this class based on a stream id match.
-export class AdbOverWebusbStream implements ByteStream {
-  private adbConnection: AdbConnectionOverWebusb;
-  private _isConnected: boolean;
-  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
-  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
-  localStreamId: number;
-  remoteStreamId = -1;
-
-  constructor(
-    adb: AdbConnectionOverWebusb,
-    localStreamId: number,
-    remoteStreamId: number,
-  ) {
-    this.adbConnection = adb;
-    this.localStreamId = localStreamId;
-    this.remoteStreamId = remoteStreamId;
-    // When the stream is created, the connection has been already established.
-    this._isConnected = true;
-  }
-
-  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
-    this.onStreamDataCallbacks.push(onStreamData);
-  }
-
-  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
-    this.onStreamCloseCallbacks.push(onStreamClose);
-  }
-
-  // Used by the connection object to signal newly received data, not exposed
-  // in the interface.
-  signalStreamData(data: Uint8Array): void {
-    for (const onStreamData of this.onStreamDataCallbacks) {
-      onStreamData(data);
-    }
-  }
-
-  // Used by the connection object to signal the stream is closed, not exposed
-  // in the interface.
-  signalStreamClosed(): void {
-    for (const onStreamClose of this.onStreamCloseCallbacks) {
-      onStreamClose();
-    }
-    this.onStreamDataCallbacks = [];
-    this.onStreamCloseCallbacks = [];
-  }
-
-  close(): void {
-    this.closeAndWaitForTeardown();
-  }
-
-  async closeAndWaitForTeardown(): Promise<void> {
-    this._isConnected = false;
-    await this.adbConnection.streamClose(this);
-  }
-
-  write(msg: string | Uint8Array): void {
-    this.adbConnection.streamWrite(msg, this);
-  }
-
-  isConnected(): boolean {
-    return this._isConnected;
-  }
-}
-
-const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
-
-class AdbMsg {
-  data: Uint8Array;
-  readonly cmd: CmdType;
-  readonly arg0: number;
-  readonly arg1: number;
-  readonly dataLen: number;
-  readonly dataChecksum: number;
-  readonly useChecksum: boolean;
-
-  constructor(
-    cmd: CmdType,
-    arg0: number,
-    arg1: number,
-    dataLen: number,
-    dataChecksum: number,
-    useChecksum = false,
-  ) {
-    assertTrue(cmd.length === 4);
-    this.cmd = cmd;
-    this.arg0 = arg0;
-    this.arg1 = arg1;
-    this.dataLen = dataLen;
-    this.data = new Uint8Array(dataLen);
-    this.dataChecksum = dataChecksum;
-    this.useChecksum = useChecksum;
-  }
-
-  static create({
-    cmd,
-    arg0,
-    arg1,
-    data,
-    useChecksum = true,
-  }: {
-    cmd: CmdType;
-    arg0: number;
-    arg1: number;
-    data?: Uint8Array | string;
-    useChecksum?: boolean;
-  }): AdbMsg {
-    const encodedData = this.encodeData(data);
-    const msg = new AdbMsg(cmd, arg0, arg1, encodedData.length, 0, useChecksum);
-    msg.data = encodedData;
-    return msg;
-  }
-
-  get dataStr() {
-    return utf8Decode(this.data);
-  }
-
-  toString() {
-    return `${this.cmd} [${this.arg0},${this.arg1}] ${this.dataStr}`;
-  }
-
-  // A brief description of the message can be found here:
-  // https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt
-  //
-  // struct amessage {
-  //     uint32_t command;    // command identifier constant
-  //     uint32_t arg0;       // first argument
-  //     uint32_t arg1;       // second argument
-  //     uint32_t data_length;// length of payload (0 is allowed)
-  //     uint32_t data_check; // checksum of data payload
-  //     uint32_t magic;      // command ^ 0xffffffff
-  // };
-  static decodeHeader(dv: DataView): AdbMsg {
-    assertTrue(dv.byteLength === ADB_MSG_SIZE);
-    const cmd = utf8Decode(dv.buffer.slice(0, 4)) as CmdType;
-    const cmdNum = dv.getUint32(0, true);
-    const arg0 = dv.getUint32(4, true);
-    const arg1 = dv.getUint32(8, true);
-    const dataLen = dv.getUint32(12, true);
-    const dataChecksum = dv.getUint32(16, true);
-    const cmdChecksum = dv.getUint32(20, true);
-    assertTrue(cmdNum === (cmdChecksum ^ 0xffffffff));
-    return new AdbMsg(cmd, arg0, arg1, dataLen, dataChecksum);
-  }
-
-  encodeHeader(): Uint8Array {
-    const buf = new Uint8Array(ADB_MSG_SIZE);
-    const dv = new DataView(buf.buffer);
-    const cmdBytes: Uint8Array = utf8Encode(this.cmd);
-    const rawMsg = AdbMsg.encodeData(this.data);
-    const checksum = this.useChecksum ? generateChecksum(rawMsg) : 0;
-    for (let i = 0; i < 4; i++) dv.setUint8(i, cmdBytes[i]);
-
-    dv.setUint32(4, this.arg0, true);
-    dv.setUint32(8, this.arg1, true);
-    dv.setUint32(12, rawMsg.byteLength, true);
-    dv.setUint32(16, checksum, true);
-    dv.setUint32(20, dv.getUint32(0, true) ^ 0xffffffff, true);
-
-    return buf;
-  }
-
-  static encodeData(data?: Uint8Array | string): Uint8Array {
-    if (data === undefined) return new Uint8Array([]);
-    if (isString(data)) return utf8Encode(data + '\0');
-    return data;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
deleted file mode 100644
index 078726f..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (C) 2022 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 {defer, Deferred} from '../../../base/deferred';
-import {assertFalse} from '../../../base/logging';
-import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
-import {RecordingError} from './recording_error_handling';
-import {ByteStream} from './recording_interfaces_v2';
-import {
-  BINARY_PUSH_FAILURE,
-  BINARY_PUSH_UNKNOWN_RESPONSE,
-} from './recording_utils';
-import {utf8Decode} from '../../../base/string_utils';
-
-// https://cs.android.com/android/platform/superproject/+/main:packages/
-// modules/adb/file_sync_protocol.h;l=144
-const MAX_SYNC_SEND_CHUNK_SIZE = 64 * 1024;
-
-// Adb does not accurately send some file permissions. If you need a special set
-// of permissions, do not rely on this value. Rather, send a shell command which
-// explicitly sets permissions, such as:
-// 'shell:chmod ${permissions} ${path}'
-const FILE_PERMISSIONS = 2 ** 15 + 0o644;
-
-// For details about the protocol, see:
-// https://cs.android.com/android/platform/superproject/+/main:packages/modules/adb/SYNC.TXT
-export class AdbFileHandler {
-  private sentByteCount = 0;
-  private isPushOngoing: boolean = false;
-
-  constructor(private byteStream: ByteStream) {}
-
-  async pushBinary(binary: Uint8Array, path: string): Promise<void> {
-    // For a given byteStream, we only support pushing one binary at a time.
-    assertFalse(this.isPushOngoing);
-    this.isPushOngoing = true;
-    const transferFinished = defer<void>();
-
-    this.byteStream.addOnStreamDataCallback((data) =>
-      this.onStreamData(data, transferFinished),
-    );
-    this.byteStream.addOnStreamCloseCallback(
-      () => (this.isPushOngoing = false),
-    );
-
-    const sendMessage = new ArrayBufferBuilder();
-    // 'SEND' is the API method used to send a file to device.
-    sendMessage.append('SEND');
-    // The remote file name is split into two parts separated by the last
-    // comma (","). The first part is the actual path, while the second is a
-    // decimal encoded file mode containing the permissions of the file on
-    // device.
-    sendMessage.append(path.length + 6);
-    sendMessage.append(path);
-    sendMessage.append(',');
-    sendMessage.append(FILE_PERMISSIONS.toString());
-    this.byteStream.write(new Uint8Array(sendMessage.toArrayBuffer()));
-
-    while (!(await this.sendNextDataChunk(binary)));
-
-    return transferFinished;
-  }
-
-  private onStreamData(data: Uint8Array, transferFinished: Deferred<void>) {
-    this.sentByteCount = 0;
-    const response = utf8Decode(data);
-    if (response.split('\n')[0].includes('FAIL')) {
-      // Sample failure response (when the file is transferred successfully
-      // but the date is not formatted correctly):
-      // 'OKAYFAIL\npath too long'
-      transferFinished.reject(
-        new RecordingError(`${BINARY_PUSH_FAILURE}: ${response}`),
-      );
-    } else if (utf8Decode(data).substring(0, 4) === 'OKAY') {
-      // In case of success, the server responds to the last request with
-      // 'OKAY'.
-      transferFinished.resolve();
-    } else {
-      throw new RecordingError(`${BINARY_PUSH_UNKNOWN_RESPONSE}: ${response}`);
-    }
-  }
-
-  private async sendNextDataChunk(binary: Uint8Array): Promise<boolean> {
-    const endPosition = Math.min(
-      this.sentByteCount + MAX_SYNC_SEND_CHUNK_SIZE,
-      binary.byteLength,
-    );
-    const chunk = await binary.slice(this.sentByteCount, endPosition);
-    // The file is sent in chunks. Each chunk is prefixed with "DATA" and the
-    // chunk length. This is repeated until the entire file is transferred. Each
-    // chunk must not be larger than 64k.
-    const chunkLength = chunk.byteLength;
-    const dataMessage = new ArrayBufferBuilder();
-    dataMessage.append('DATA');
-    dataMessage.append(chunkLength);
-    dataMessage.append(
-      new Uint8Array(chunk.buffer, chunk.byteOffset, chunkLength),
-    );
-
-    this.sentByteCount += chunkLength;
-    const isDone = this.sentByteCount === binary.byteLength;
-
-    if (isDone) {
-      // When the file is transferred a sync request "DONE" is sent, together
-      // with a timestamp, representing the last modified time for the file. The
-      // server responds to this last request.
-      dataMessage.append('DONE');
-      // We send the date in seconds.
-      dataMessage.append(Math.floor(Date.now() / 1000));
-    }
-    this.byteStream.write(new Uint8Array(dataMessage.toArrayBuffer()));
-    return isDone;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
deleted file mode 100644
index 0ce297b..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2022 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 {assetSrc} from '../../../../base/assets';
-import {AdbKey} from './adb_auth';
-
-function isPasswordCredential(
-  cred: Credential | null,
-): cred is PasswordCredential {
-  return cred !== null && cred.type === 'password';
-}
-
-function hasPasswordCredential() {
-  return 'PasswordCredential' in window;
-}
-
-// how long we will store the key in memory
-const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes
-
-// Update credential store with the given key.
-export async function maybeStoreKey(key: AdbKey): Promise<void> {
-  if (!hasPasswordCredential()) {
-    return;
-  }
-  const credential = new PasswordCredential({
-    id: 'webusb-adb-key',
-    password: key.serializeKey(),
-    name: 'WebUSB ADB Key',
-    iconURL: assetSrc('assets/favicon.png'),
-  });
-  // The 'Save password?' Chrome dialogue only appears if the key is
-  // not already stored in Chrome.
-  await navigator.credentials.store(credential);
-  // 'preventSilentAccess' guarantees the user is always notified when
-  // credentials are accessed. Sometimes the user is asked to click a button
-  // and other times only a notification is shown temporarily.
-  await navigator.credentials.preventSilentAccess();
-}
-
-export class AdbKeyManager {
-  private key?: AdbKey;
-  // Id of timer used to expire the key kept in memory.
-  private keyInMemoryTimerId?: ReturnType<typeof setTimeout>;
-
-  // Finds a key, by priority:
-  // - looking in memory (i.e. this.key)
-  // - looking in the credential store
-  // - and finally creating one from scratch if needed
-  async getKey(): Promise<AdbKey> {
-    // 1. If we have a private key in memory, we return it.
-    if (this.key) {
-      return this.key;
-    }
-
-    // 2. We try to get the private key from the browser.
-    // The mediation is set as 'optional', because we use
-    // 'preventSilentAccess', which sometimes requests the user to click
-    // on a button to allow the auth, but sometimes only shows a
-    // notification and does not require the user to click on anything.
-    // If we had set mediation to 'required', the user would have been
-    // asked to click on a button every time.
-    if (hasPasswordCredential()) {
-      const options: PasswordCredentialRequestOptions = {
-        password: true,
-        mediation: 'optional',
-      };
-      const credential = await navigator.credentials.get(options);
-      if (isPasswordCredential(credential)) {
-        return this.assignKey(AdbKey.DeserializeKey(credential.password));
-      }
-    }
-
-    // 3. We generate a new key pair.
-    return this.assignKey(await AdbKey.GenerateNewKeyPair());
-  }
-
-  // Assigns the key a new value, sets a timeout for storing the key in memory
-  // and then returns the new key.
-  private assignKey(key: AdbKey): AdbKey {
-    this.key = key;
-    if (this.keyInMemoryTimerId) {
-      clearTimeout(this.keyInMemoryTimerId);
-    }
-    this.keyInMemoryTimerId = setTimeout(
-      () => (this.key = undefined),
-      KEY_IN_MEMORY_TIMEOUT,
-    );
-    return key;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
deleted file mode 100644
index 55f87ce..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright (C) 2022 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 {defer, Deferred} from '../../../base/deferred';
-import {assertExists, assertTrue} from '../../../base/logging';
-import {binaryDecode, binaryEncode} from '../../../base/string_utils';
-import {
-  ChromeExtensionMessage,
-  isChromeExtensionError,
-  isChromeExtensionStatus,
-  isGetCategoriesResponse,
-} from '../chrome_proxy_record_controller';
-import {
-  isDisableTracingResponse,
-  isEnableTracingResponse,
-  isFreeBuffersResponse,
-  isGetTraceStatsResponse,
-  isReadBuffersResponse,
-} from '../consumer_port_types';
-import {
-  EnableTracingRequest,
-  IBufferStats,
-  ISlice,
-  TraceConfig,
-} from '../protos';
-import {RecordingError} from './recording_error_handling';
-import {
-  TracingSession,
-  TracingSessionListener,
-} from './recording_interfaces_v2';
-import {
-  BUFFER_USAGE_INCORRECT_FORMAT,
-  BUFFER_USAGE_NOT_ACCESSIBLE,
-  EXTENSION_ID,
-  MALFORMED_EXTENSION_MESSAGE,
-} from './recording_utils';
-
-// This class implements the protocol described in
-// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
-// However, with the Chrome extension we communicate using JSON messages.
-export class ChromeTracedTracingSession implements TracingSession {
-  // Needed for ReadBufferResponse: all the trace packets are split into
-  // several slices. |partialPacket| is the buffer for them. Once we receive a
-  // slice with the flag |lastSliceForPacket|, a new packet is created.
-  private partialPacket: ISlice[] = [];
-
-  // For concurrent calls to 'GetCategories', we return the same value.
-  private pendingGetCategoriesMessage?: Deferred<string[]>;
-
-  private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
-
-  // Port through which we communicate with the extension.
-  private chromePort: chrome.runtime.Port;
-  // True when Perfetto is connected via the port to the tracing session.
-  private isPortConnected: boolean;
-
-  constructor(private tracingSessionListener: TracingSessionListener) {
-    this.chromePort = chrome.runtime.connect(EXTENSION_ID);
-    this.isPortConnected = true;
-  }
-
-  start(config: TraceConfig): void {
-    if (!this.isPortConnected) return;
-    const duration = config.durationMs;
-    this.tracingSessionListener.onStatus(
-      `Recording in progress${
-        duration ? ' for ' + duration.toString() + ' ms' : ''
-      }...`,
-    );
-
-    const enableTracingRequest = new EnableTracingRequest();
-    enableTracingRequest.traceConfig = config;
-    const enableTracingRequestProto = binaryEncode(
-      EnableTracingRequest.encode(enableTracingRequest).finish(),
-    );
-    this.chromePort.postMessage({
-      method: 'EnableTracing',
-      requestData: enableTracingRequestProto,
-    });
-  }
-
-  // The 'cancel' method will end the tracing session and will NOT return the
-  // trace. Therefore, we do not need to keep the connection open.
-  cancel(): void {
-    if (!this.isPortConnected) return;
-    this.terminateConnection();
-  }
-
-  // The 'stop' method will end the tracing session and cause the trace to be
-  // returned via a callback. We maintain the connection to the target so we can
-  // extract the trace.
-  // See 'DisableTracing' in:
-  // https://perfetto.dev/docs/design-docs/life-of-a-tracing-session
-  stop(): void {
-    if (!this.isPortConnected) return;
-    this.chromePort.postMessage({method: 'DisableTracing'});
-  }
-
-  getCategories(): Promise<string[]> {
-    if (!this.isPortConnected) {
-      throw new RecordingError(
-        'Attempting to get categories from a ' +
-          'disconnected tracing session.',
-      );
-    }
-    if (this.pendingGetCategoriesMessage) {
-      return this.pendingGetCategoriesMessage;
-    }
-
-    this.chromePort.postMessage({method: 'GetCategories'});
-    return (this.pendingGetCategoriesMessage = defer<string[]>());
-  }
-
-  async getTraceBufferUsage(): Promise<number> {
-    if (!this.isPortConnected) return 0;
-    const bufferStats = await this.getBufferStats();
-    let percentageUsed = -1;
-    for (const buffer of bufferStats) {
-      const used = assertExists(buffer.bytesWritten);
-      const total = assertExists(buffer.bufferSize);
-      if (total >= 0) {
-        percentageUsed = Math.max(percentageUsed, used / total);
-      }
-    }
-
-    if (percentageUsed === -1) {
-      throw new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT);
-    }
-    return percentageUsed;
-  }
-
-  initConnection(): void {
-    this.chromePort.onMessage.addListener((message: ChromeExtensionMessage) => {
-      this.handleExtensionMessage(message);
-    });
-  }
-
-  private getBufferStats(): Promise<IBufferStats[]> {
-    this.chromePort.postMessage({method: 'GetTraceStats'});
-
-    const statsMessage = defer<IBufferStats[]>();
-    this.pendingStatsMessages.push(statsMessage);
-    return statsMessage;
-  }
-
-  private terminateConnection(): void {
-    this.chromePort.postMessage({method: 'FreeBuffers'});
-    this.clearState();
-  }
-
-  private clearState() {
-    this.chromePort.disconnect();
-    this.isPortConnected = false;
-    for (const statsMessage of this.pendingStatsMessages) {
-      statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
-    }
-    this.pendingStatsMessages = [];
-    this.pendingGetCategoriesMessage = undefined;
-  }
-
-  private handleExtensionMessage(message: ChromeExtensionMessage) {
-    if (isChromeExtensionError(message)) {
-      this.terminateConnection();
-      this.tracingSessionListener.onError(message.error);
-    } else if (isChromeExtensionStatus(message)) {
-      this.tracingSessionListener.onStatus(message.status);
-    } else if (isReadBuffersResponse(message)) {
-      if (!message.slices) {
-        return;
-      }
-      for (const messageSlice of message.slices) {
-        // The extension sends the binary data as a string.
-        // see http://shortn/_oPmO2GT6Vb
-        if (typeof messageSlice.data !== 'string') {
-          throw new RecordingError(MALFORMED_EXTENSION_MESSAGE);
-        }
-        const decodedSlice = {
-          data: binaryDecode(messageSlice.data),
-        };
-        this.partialPacket.push(decodedSlice);
-        if (messageSlice.lastSliceForPacket) {
-          let bufferSize = 0;
-          for (const slice of this.partialPacket) {
-            bufferSize += slice.data!.length;
-          }
-
-          const completeTrace = new Uint8Array(bufferSize);
-          let written = 0;
-          for (const slice of this.partialPacket) {
-            const data = slice.data!;
-            completeTrace.set(data, written);
-            written += data.length;
-          }
-          // The trace already comes encoded as a proto.
-          this.tracingSessionListener.onTraceData(completeTrace);
-          this.terminateConnection();
-        }
-      }
-    } else if (isGetCategoriesResponse(message)) {
-      assertExists(this.pendingGetCategoriesMessage).resolve(
-        message.categories,
-      );
-      this.pendingGetCategoriesMessage = undefined;
-    } else if (isEnableTracingResponse(message)) {
-      // Once the service notifies us that a tracing session is enabled,
-      // we can start streaming the response using 'ReadBuffers'.
-      this.chromePort.postMessage({method: 'ReadBuffers'});
-    } else if (isGetTraceStatsResponse(message)) {
-      const maybePendingStatsMessage = this.pendingStatsMessages.shift();
-      if (maybePendingStatsMessage) {
-        maybePendingStatsMessage.resolve(
-          message?.traceStats?.bufferStats || [],
-        );
-      }
-    } else if (isFreeBuffersResponse(message)) {
-      // No action required. If we successfully read a whole trace,
-      // we close the connection. Alternatively, if the tracing finishes
-      // with an exception or if the user cancels it, we also close the
-      // connection.
-    } else {
-      assertTrue(isDisableTracingResponse(message));
-      // No action required. Same reasoning as for FreeBuffers.
-    }
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
deleted file mode 100644
index a03b791..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) 2022 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 {defer} from '../../../base/deferred';
-import {
-  ByteStream,
-  OnStreamCloseCallback,
-  OnStreamDataCallback,
-} from './recording_interfaces_v2';
-
-// A HostOsByteStream instantiates a websocket connection to the host OS.
-// It exposes an API to write commands to this websocket and read its output.
-export class HostOsByteStream implements ByteStream {
-  // handshakeSignal will be resolved with the stream when the websocket
-  // connection becomes open.
-  private handshakeSignal = defer<HostOsByteStream>();
-  private _isConnected: boolean = false;
-  private websocket: WebSocket;
-  private onStreamDataCallbacks: OnStreamDataCallback[] = [];
-  private onStreamCloseCallbacks: OnStreamCloseCallback[] = [];
-
-  private constructor(websocketUrl: string) {
-    this.websocket = new WebSocket(websocketUrl);
-    this.websocket.onmessage = this.onMessage.bind(this);
-    this.websocket.onopen = this.onOpen.bind(this);
-  }
-
-  addOnStreamDataCallback(onStreamData: OnStreamDataCallback): void {
-    this.onStreamDataCallbacks.push(onStreamData);
-  }
-
-  addOnStreamCloseCallback(onStreamClose: OnStreamCloseCallback): void {
-    this.onStreamCloseCallbacks.push(onStreamClose);
-  }
-
-  close(): void {
-    this.websocket.close();
-    for (const onStreamClose of this.onStreamCloseCallbacks) {
-      onStreamClose();
-    }
-    this.onStreamDataCallbacks = [];
-    this.onStreamCloseCallbacks = [];
-  }
-
-  async closeAndWaitForTeardown(): Promise<void> {
-    this.close();
-  }
-
-  isConnected(): boolean {
-    return this._isConnected;
-  }
-
-  write(msg: string | Uint8Array): void {
-    this.websocket.send(msg);
-  }
-
-  private async onMessage(evt: MessageEvent) {
-    for (const onStreamData of this.onStreamDataCallbacks) {
-      const arrayBufferResponse = await evt.data.arrayBuffer();
-      onStreamData(new Uint8Array(arrayBufferResponse));
-    }
-  }
-
-  private onOpen() {
-    this._isConnected = true;
-    this.handshakeSignal.resolve(this);
-  }
-
-  static create(websocketUrl: string): Promise<HostOsByteStream> {
-    return new HostOsByteStream(websocketUrl).handshakeSignal;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
index 471ebc8..d6d69ad 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -15,15 +15,9 @@
 import {assertExists, assertTrue} from '../../../base/logging';
 import {currentDateHourAndMinute} from '../../../base/time';
 import {RecordingManager} from '../recording_manager';
-import {autosaveConfigStore} from '../record_config';
-import {
-  DEFAULT_ADB_WEBSOCKET_URL,
-  DEFAULT_TRACED_WEBSOCKET_URL,
-} from '../recording_ui_utils';
 import {couldNotClaimInterface} from '../reset_interface_modal';
 import {TraceConfig} from '../protos';
 import {TRACE_SUFFIX} from '../../../public/trace';
-import {genTraceConfig} from './recording_config_utils';
 import {RecordingError, showRecordingModal} from './recording_error_handling';
 import {
   RecordingTargetV2,
@@ -31,21 +25,8 @@
   TracingSession,
   TracingSessionListener,
 } from './recording_interfaces_v2';
-import {
-  BUFFER_USAGE_NOT_ACCESSIBLE,
-  RECORDING_IN_PROGRESS,
-} from './recording_utils';
-import {
-  ANDROID_WEBSOCKET_TARGET_FACTORY,
-  AndroidWebsocketTargetFactory,
-} from './target_factories/android_websocket_target_factory';
-import {ANDROID_WEBUSB_TARGET_FACTORY} from './target_factories/android_webusb_target_factory';
-import {
-  HOST_OS_TARGET_FACTORY,
-  HostOsTargetFactory,
-} from './target_factories/host_os_target_factory';
+import {RECORDING_IN_PROGRESS} from './recording_utils';
 import {targetFactoryRegistry} from './target_factory_registry';
-import {scheduleFullRedraw} from '../../../widgets/raf';
 import {App} from '../../../public/app';
 
 // The recording page can be in any of these states. It can transition between
@@ -194,57 +175,6 @@
       );
     }
   }
-
-  cancel() {
-    if (this.tracingSession) {
-      this.tracingSession.cancel();
-    } else {
-      // In some cases, the tracingSession may not be available to the
-      // TracingSessionWrapper when the user cancels it.
-      // For instance:
-      //  1. The user clicked 'Start'.
-      //  2. They clicked 'Stop' without authorizing on the device.
-      //  3. They clicked 'Start'.
-      //  4. They authorized on the device.
-      // In these cases, we want to cancel the tracing session as soon as it
-      // becomes available. Therefore, we keep the `isCancelled` boolean and
-      // check it when we receive the tracing session.
-      this.isCancelled = true;
-    }
-    this.controller.maybeClearRecordingState(this);
-  }
-
-  stop() {
-    const stateGeneratioNr = this.controller.getStateGeneration();
-    if (this.tracingSession) {
-      this.tracingSession.stop();
-      this.controller.maybeSetState(
-        this,
-        RecordingState.WAITING_FOR_TRACE_DISPLAY,
-        stateGeneratioNr,
-      );
-    } else {
-      // In some cases, the tracingSession may not be available to the
-      // TracingSessionWrapper when the user stops it.
-      // For instance:
-      //  1. The user clicked 'Start'.
-      //  2. They clicked 'Stop' without authorizing on the device.
-      //  3. They clicked 'Start'.
-      //  4. They authorized on the device.
-      // In these cases, we want to cancel the tracing session as soon as it
-      // becomes available. Therefore, we keep the `isCancelled` boolean and
-      // check it when we receive the tracing session.
-      this.isCancelled = true;
-      this.controller.maybeClearRecordingState(this);
-    }
-  }
-
-  getTraceBufferUsage(): Promise<number> {
-    if (!this.tracingSession) {
-      throw new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE);
-    }
-    return this.tracingSession.getTraceBufferUsage();
-  }
 }
 
 // Keeps track of the state the Ui is in. Has methods which are executed on
@@ -262,8 +192,6 @@
   // (Ex: Android) it is only created after we have succesfully authenticated
   // with the target.
   private tracingSessionWrapper?: TracingSessionWrapper = undefined;
-  // How much of the buffer is used for the current tracing session.
-  private bufferUsagePercentage: number = 0;
   // A counter for state modifications. We use this to ensure that state
   // transitions don't override one another in async functions.
   private stateGeneration = 0;
@@ -273,14 +201,6 @@
     this.recMgr = recMgr;
   }
 
-  getBufferUsagePercentage(): number {
-    return this.bufferUsagePercentage;
-  }
-
-  getState(): RecordingState {
-    return this.state;
-  }
-
   getStateGeneration(): number {
     return this.stateGeneration;
   }
@@ -298,7 +218,7 @@
     }
     this.setState(state);
     this.recMgr.setRecordingStatus(undefined);
-    scheduleFullRedraw();
+    this.app.raf.scheduleFullRedraw();
   }
 
   maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
@@ -392,148 +312,22 @@
 
     if (!this.target) {
       this.setState(RecordingState.NO_TARGET);
-      scheduleFullRedraw();
+      this.app.raf.scheduleFullRedraw();
       return;
     }
     this.setState(RecordingState.TARGET_SELECTED);
-    scheduleFullRedraw();
+    this.app.raf.scheduleFullRedraw();
 
     this.tracingSessionWrapper = this.createTracingSessionWrapper(this.target);
     this.tracingSessionWrapper.fetchTargetInfo();
   }
 
-  async addAndroidDevice(): Promise<void> {
-    try {
-      const target = await targetFactoryRegistry
-        .get(ANDROID_WEBUSB_TARGET_FACTORY)
-        .connectNewTarget();
-      this.selectTarget(target);
-    } catch (e) {
-      if (e instanceof RecordingError) {
-        showRecordingModal(e.message);
-      } else {
-        throw e;
-      }
-    }
-  }
-
-  onTargetSelection(targetName: string): void {
-    assertTrue(
-      RecordingState.NO_TARGET <= this.state &&
-        this.state < RecordingState.RECORDING,
-    );
-    const allTargets = targetFactoryRegistry.listTargets();
-    this.selectTarget(allTargets.find((t) => t.getInfo().name === targetName));
-  }
-
-  onStartRecordingPressed(): void {
-    assertTrue(RecordingState.TARGET_INFO_DISPLAYED === this.state);
-    location.href = '#!/record/instructions';
-    autosaveConfigStore.save(this.recMgr.state.recordConfig);
-
-    const target = this.getTarget();
-    const targetInfo = target.getInfo();
-    this.app.analytics.logEvent(
-      'Record Trace',
-      `Record trace (${targetInfo.targetType})`,
-    );
-    const traceConfig = genTraceConfig(
-      this.recMgr.state.recordConfig,
-      targetInfo,
-    );
-
-    this.tracingSessionWrapper = this.createTracingSessionWrapper(target);
-    this.tracingSessionWrapper.start(traceConfig);
-  }
-
-  onCancel() {
-    assertTrue(
-      RecordingState.AUTH_P2 <= this.state &&
-        this.state <= RecordingState.RECORDING,
-    );
-    // The 'Cancel' button will only be shown after a `tracingSessionWrapper`
-    // is created.
-    this.getTracingSessionWrapper().cancel();
-  }
-
-  onStop() {
-    assertTrue(
-      RecordingState.AUTH_P2 <= this.state &&
-        this.state <= RecordingState.RECORDING,
-    );
-    // The 'Stop' button will only be shown after a `tracingSessionWrapper`
-    // is created.
-    this.getTracingSessionWrapper().stop();
-  }
-
-  async fetchBufferUsage() {
-    assertTrue(this.state >= RecordingState.AUTH_P2);
-    if (!this.tracingSessionWrapper) return;
-    const session = this.tracingSessionWrapper;
-
-    try {
-      const usage = await session.getTraceBufferUsage();
-      if (this.tracingSessionWrapper === session) {
-        this.bufferUsagePercentage = usage;
-      }
-    } catch (e) {
-      // We ignore RecordingErrors because they are not necessary for the trace
-      // to be successfully collected.
-      if (!(e instanceof RecordingError)) {
-        throw e;
-      }
-    }
-    // We redraw if:
-    // 1. We received a correct buffer usage value.
-    // 2. We receive a RecordingError.
-    scheduleFullRedraw();
-  }
-
-  initFactories() {
-    assertTrue(this.state <= RecordingState.TARGET_INFO_DISPLAYED);
-    for (const targetFactory of targetFactoryRegistry.listTargetFactories()) {
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (targetFactory) {
-        targetFactory.setOnTargetChange(this.onTargetChange.bind(this));
-      }
-    }
-
-    if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
-      const websocketTargetFactory = targetFactoryRegistry.get(
-        ANDROID_WEBSOCKET_TARGET_FACTORY,
-      ) as AndroidWebsocketTargetFactory;
-      websocketTargetFactory.tryEstablishWebsocket(DEFAULT_ADB_WEBSOCKET_URL);
-    }
-    if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
-      const websocketTargetFactory = targetFactoryRegistry.get(
-        HOST_OS_TARGET_FACTORY,
-      ) as HostOsTargetFactory;
-      websocketTargetFactory.tryEstablishWebsocket(
-        DEFAULT_TRACED_WEBSOCKET_URL,
-      );
-    }
-  }
-
-  shouldShowTargetSelection(): boolean {
-    return (
-      RecordingState.NO_TARGET <= this.state &&
-      this.state < RecordingState.RECORDING
-    );
-  }
-
-  shouldShowStopCancelButtons(): boolean {
-    return (
-      RecordingState.AUTH_P2 <= this.state &&
-      this.state <= RecordingState.RECORDING
-    );
-  }
-
   private onTargetChange() {
     const allTargets = targetFactoryRegistry.listTargets();
     // If the change happens for an existing target, the controller keeps the
     // currently selected target in focus.
     if (this.target && allTargets.includes(this.target)) {
-      scheduleFullRedraw();
+      this.app.raf.scheduleFullRedraw();
       return;
     }
     // If the change happens to a new target or the controller does not have a
@@ -548,30 +342,16 @@
   }
 
   private clearRecordingState(): void {
-    this.bufferUsagePercentage = 0;
     this.tracingSessionWrapper = undefined;
     this.setState(RecordingState.TARGET_INFO_DISPLAYED);
     this.recMgr.setRecordingStatus(undefined);
     // Redrawing because this method has changed the RecordingState, which will
     // affect the display of the record_page.
-    scheduleFullRedraw();
+    this.app.raf.scheduleFullRedraw();
   }
 
   private setState(state: RecordingState) {
     this.state = state;
     this.stateGeneration += 1;
   }
-
-  private getTarget(): RecordingTargetV2 {
-    assertTrue(RecordingState.TARGET_INFO_DISPLAYED === this.state);
-    return assertExists(this.target);
-  }
-
-  private getTracingSessionWrapper(): TracingSessionWrapper {
-    assertTrue(
-      RecordingState.ASK_TO_FORCE_P2 <= this.state &&
-        this.state <= RecordingState.RECORDING,
-    );
-    return assertExists(this.tracingSessionWrapper);
-  }
 }
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
deleted file mode 100644
index 03cda1f..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (C) 2022 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 {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {
-  buildAbdWebsocketCommand,
-  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
-} from '../recording_utils';
-import {AndroidWebsocketTarget} from '../targets/android_websocket_target';
-
-export const ANDROID_WEBSOCKET_TARGET_FACTORY = 'AndroidWebsocketTargetFactory';
-
-// https://cs.android.com/android/platform/superproject/+/main:packages/
-// modules/adb/SERVICES.TXT;l=135
-const PREFIX_LENGTH = 4;
-
-// information received over the websocket regarding a device
-// Ex: "${serialNumber} authorized"
-interface ListedDevice {
-  serialNumber: string;
-  // Full list of connection states can be seen at:
-  // go/codesearch/android/packages/modules/adb/adb.cpp;l=115-139
-  connectionState: string;
-}
-
-// Contains the result of parsing a message received over websocket.
-interface ParsingResult {
-  listedDevices: ListedDevice[];
-  messageRemainder: string;
-}
-
-// We issue the command 'track-devices' which will encode the short form
-// of the device:
-// see go/codesearch/android/packages/modules/adb/services.cpp;l=244-245
-// and go/codesearch/android/packages/modules/adb/transport.cpp;l=1417-1420
-// Therefore a line will contain solely the device serial number and the
-// connectionState (and no other properties).
-function parseListedDevice(line: string): ListedDevice | undefined {
-  const parts = line.split('\t');
-  if (parts.length === 2) {
-    return {
-      serialNumber: parts[0],
-      connectionState: parts[1],
-    };
-  }
-  return undefined;
-}
-
-export function parseWebsocketResponse(message: string): ParsingResult {
-  // A response we receive on the websocket contains multiple messages:
-  // "{m1.length}{m1.payload}{m2.length}{m2.payload}..."
-  // where m1, m2 are messages
-  // Each message has the form:
-  // "{message.length}SN1\t${connectionState1}\nSN2\t${connectionState2}\n..."
-  // where SN1, SN2 are device serial numbers
-  // and connectionState1, connectionState2 are adb connection states, created
-  // here: go/codesearch/android/packages/modules/adb/adb.cpp;l=115-139
-  const latestStatusByDevice: Map<string, string> = new Map();
-  while (message.length >= PREFIX_LENGTH) {
-    const payloadLength = parseInt(message.substring(0, PREFIX_LENGTH), 16);
-    const prefixAndPayloadLength = PREFIX_LENGTH + payloadLength;
-    if (message.length < prefixAndPayloadLength) {
-      break;
-    }
-
-    const payload = message.substring(PREFIX_LENGTH, prefixAndPayloadLength);
-    for (const line of payload.split('\n')) {
-      const listedDevice = parseListedDevice(line);
-      if (listedDevice) {
-        // We overwrite previous states for the same serial number.
-        latestStatusByDevice.set(
-          listedDevice.serialNumber,
-          listedDevice.connectionState,
-        );
-      }
-    }
-    message = message.substring(prefixAndPayloadLength);
-  }
-  const listedDevices: ListedDevice[] = [];
-  for (const [
-    serialNumber,
-    connectionState,
-  ] of latestStatusByDevice.entries()) {
-    listedDevices.push({serialNumber, connectionState});
-  }
-  return {listedDevices, messageRemainder: message};
-}
-
-export class WebsocketConnection {
-  private targets: Map<string, AndroidWebsocketTarget> = new Map<
-    string,
-    AndroidWebsocketTarget
-  >();
-  private pendingData: string = '';
-
-  constructor(
-    private websocket: WebSocket,
-    private maybeClearConnection: (connection: WebsocketConnection) => void,
-    private onTargetChange: OnTargetChangeCallback,
-  ) {
-    this.initWebsocket();
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    return Array.from(this.targets.values());
-  }
-
-  // Setup websocket callbacks.
-  initWebsocket(): void {
-    this.websocket.onclose = (ev: CloseEvent) => {
-      if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
-        console.info(
-          `It's safe to ignore the 'WebSocket connection to ${this.websocket.url} error above, if present. It occurs when ` +
-            'checking the connection to the local Websocket server.',
-        );
-      }
-      this.maybeClearConnection(this);
-      this.close();
-    };
-
-    // once the websocket is open, we start tracking the devices
-    this.websocket.onopen = () => {
-      this.websocket.send(buildAbdWebsocketCommand('host:track-devices'));
-    };
-
-    this.websocket.onmessage = async (evt: MessageEvent) => {
-      let resp = await evt.data.text();
-      if (resp.substr(0, 4) === 'OKAY') {
-        resp = resp.substr(4);
-      }
-      const parsingResult = parseWebsocketResponse(this.pendingData + resp);
-      this.pendingData = parsingResult.messageRemainder;
-      this.trackDevices(parsingResult.listedDevices);
-    };
-  }
-
-  close() {
-    // The websocket connection may have already been closed by the websocket
-    // server.
-    if (this.websocket.readyState === this.websocket.OPEN) {
-      this.websocket.close();
-    }
-    // Disconnect all the targets, to release all the websocket connections that
-    // they hold and end their tracing sessions.
-    for (const target of this.targets.values()) {
-      target.disconnect();
-    }
-    this.targets.clear();
-
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (this.onTargetChange) {
-      this.onTargetChange();
-    }
-  }
-
-  getUrl() {
-    return this.websocket.url;
-  }
-
-  // Handle messages received over the websocket regarding devices connecting
-  // or disconnecting.
-  private trackDevices(listedDevices: ListedDevice[]) {
-    // When a SN becomes offline, we should remove it from the list
-    // of targets. Otherwise, we should check if it maps to a target. If the
-    // SN does not map to a target, we should create one for it.
-    let targetsUpdated = false;
-    for (const listedDevice of listedDevices) {
-      if (['offline', 'unknown'].includes(listedDevice.connectionState)) {
-        const target = this.targets.get(listedDevice.serialNumber);
-        if (target === undefined) {
-          continue;
-        }
-        target.disconnect();
-        this.targets.delete(listedDevice.serialNumber);
-        targetsUpdated = true;
-      } else if (!this.targets.has(listedDevice.serialNumber)) {
-        this.targets.set(
-          listedDevice.serialNumber,
-          new AndroidWebsocketTarget(
-            listedDevice.serialNumber,
-            this.websocket.url,
-            this.onTargetChange,
-          ),
-        );
-        targetsUpdated = true;
-      }
-    }
-
-    // Notify the calling code that the list of targets has been updated.
-    if (targetsUpdated) {
-      this.onTargetChange();
-    }
-  }
-}
-
-export class AndroidWebsocketTargetFactory implements TargetFactory {
-  readonly kind = ANDROID_WEBSOCKET_TARGET_FACTORY;
-  private onTargetChange: OnTargetChangeCallback = () => {};
-  private websocketConnection?: WebsocketConnection;
-
-  getName() {
-    return 'Android Websocket';
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    return this.websocketConnection
-      ? this.websocketConnection.listTargets()
-      : [];
-  }
-
-  listRecordingProblems(): string[] {
-    return [];
-  }
-
-  // This interface method can not return anything because a websocket target
-  // can not be created on user input. It can only be created when the websocket
-  // server detects a new target.
-  connectNewTarget(): Promise<RecordingTargetV2> {
-    return Promise.reject(
-      new Error(
-        'The websocket can only automatically connect targets ' +
-          'when they become available.',
-      ),
-    );
-  }
-
-  tryEstablishWebsocket(websocketUrl: string) {
-    if (this.websocketConnection) {
-      if (this.websocketConnection.getUrl() === websocketUrl) {
-        return;
-      } else {
-        this.websocketConnection.close();
-      }
-    }
-
-    const websocket = new WebSocket(websocketUrl);
-    this.websocketConnection = new WebsocketConnection(
-      websocket,
-      this.maybeClearConnection,
-      this.onTargetChange,
-    );
-  }
-
-  maybeClearConnection(connection: WebsocketConnection): void {
-    if (this.websocketConnection === connection) {
-      this.websocketConnection = undefined;
-    }
-  }
-
-  setOnTargetChange(onTargetChange: OnTargetChangeCallback) {
-    this.onTargetChange = onTargetChange;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
deleted file mode 100644
index 80d3dcd..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2022 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 {parseWebsocketResponse} from './android_websocket_target_factory';
-
-test('parse device disconnection', () => {
-  const message = '001702121FQC20XXXX\toffline\n';
-  const response = parseWebsocketResponse(message);
-  expect(response.messageRemainder).toEqual('');
-  expect(response.listedDevices.length).toEqual(1);
-  expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
-  expect(response.listedDevices[0].connectionState).toEqual('offline');
-});
-
-test('parse two devices connected in the same message', () => {
-  const message = '003202121FQC20XXXX\tdevice\n06131FDD40YYYY\tunauthorized\n';
-  const response = parseWebsocketResponse(message);
-  expect(response.messageRemainder).toEqual('');
-  expect(response.listedDevices.length).toEqual(2);
-  expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
-  expect(response.listedDevices[0].connectionState).toEqual('device');
-  expect(response.listedDevices[1].serialNumber).toEqual('06131FDD40YYYY');
-  expect(response.listedDevices[1].connectionState).toEqual('unauthorized');
-});
-
-test('parse device connection in multiple messages', () => {
-  const message =
-    '001702121FQC20XXXX\toffline\n001602121FQC20XXXX\tdevice\n' +
-    '001602121FQC20XXXX\tdevice\n';
-  const response = parseWebsocketResponse(message);
-  expect(response.messageRemainder).toEqual('');
-  expect(response.listedDevices.length).toEqual(1);
-  expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
-  expect(response.listedDevices[0].connectionState).toEqual('device');
-});
-
-test('parse with remainder', () => {
-  const remainder = 'FFFFsome_other_stuff';
-  const message = `001602121FQC20XXXX\tdevice\n${remainder}`;
-  const response = parseWebsocketResponse(message);
-  expect(response.messageRemainder).toEqual(remainder);
-  expect(response.listedDevices.length).toEqual(1);
-  expect(response.listedDevices[0].serialNumber).toEqual('02121FQC20XXXX');
-  expect(response.listedDevices[0].connectionState).toEqual('device');
-});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
deleted file mode 100644
index a969c31..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-// Copyright (C) 2022 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 {getErrorMessage} from '../../../../base/errors';
-import {assertExists} from '../../../../base/logging';
-import {AdbKeyManager} from '../auth/adb_key_manager';
-import {RecordingError} from '../recording_error_handling';
-import {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils';
-import {AndroidWebusbTarget} from '../targets/android_webusb_target';
-
-export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory';
-const SERIAL_NUMBER_ISSUE = 'an invalid serial number';
-const ADB_INTERFACE_ISSUE = 'an incompatible adb interface';
-
-interface DeviceValidity {
-  isValid: boolean;
-  issues: string[];
-}
-
-function createDeviceErrorMessage(device: USBDevice, issue: string): string {
-  const productName = device.productName;
-  return `USB device${productName ? ' ' + productName : ''} has ${issue}`;
-}
-
-export class AndroidWebusbTargetFactory implements TargetFactory {
-  readonly kind = ANDROID_WEBUSB_TARGET_FACTORY;
-  onTargetChange: OnTargetChangeCallback = () => {};
-  private recordingProblems: string[] = [];
-  private targets: Map<string, AndroidWebusbTarget> = new Map<
-    string,
-    AndroidWebusbTarget
-  >();
-  // AdbKeyManager should only be instantiated once, so we can use the same key
-  // for all devices.
-  private keyManager: AdbKeyManager = new AdbKeyManager();
-
-  constructor(private usb: USB) {
-    this.init();
-  }
-
-  getName() {
-    return 'Android WebUsb';
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    return Array.from(this.targets.values());
-  }
-
-  listRecordingProblems(): string[] {
-    return this.recordingProblems;
-  }
-
-  async connectNewTarget(): Promise<RecordingTargetV2> {
-    let device: USBDevice;
-    try {
-      device = await this.usb.requestDevice({filters: [ADB_DEVICE_FILTER]});
-    } catch (e) {
-      throw new RecordingError(getErrorMessage(e));
-    }
-
-    const deviceValid = this.checkDeviceValidity(device);
-    if (!deviceValid.isValid) {
-      throw new RecordingError(deviceValid.issues.join('\n'));
-    }
-
-    const androidTarget = new AndroidWebusbTarget(
-      device,
-      this.keyManager,
-      this.onTargetChange,
-    );
-    this.targets.set(assertExists(device.serialNumber), androidTarget);
-    return androidTarget;
-  }
-
-  setOnTargetChange(onTargetChange: OnTargetChangeCallback) {
-    this.onTargetChange = onTargetChange;
-  }
-
-  private async init() {
-    let devices: USBDevice[] = [];
-    try {
-      devices = await this.usb.getDevices();
-    } catch (_) {
-      return; // WebUSB not available or disallowed in iframe.
-    }
-
-    for (const device of devices) {
-      if (this.checkDeviceValidity(device).isValid) {
-        this.targets.set(
-          assertExists(device.serialNumber),
-          new AndroidWebusbTarget(device, this.keyManager, this.onTargetChange),
-        );
-      }
-    }
-
-    this.usb.addEventListener('connect', (ev: USBConnectionEvent) => {
-      if (this.checkDeviceValidity(ev.device).isValid) {
-        this.targets.set(
-          assertExists(ev.device.serialNumber),
-          new AndroidWebusbTarget(
-            ev.device,
-            this.keyManager,
-            this.onTargetChange,
-          ),
-        );
-        this.onTargetChange();
-      }
-    });
-
-    this.usb.addEventListener('disconnect', async (ev: USBConnectionEvent) => {
-      // We don't check device validity when disconnecting because if the device
-      // is invalid we would not have connected in the first place.
-      const serialNumber = assertExists(ev.device.serialNumber);
-      await assertExists(this.targets.get(serialNumber)).disconnect(
-        `Device with serial ${serialNumber} was disconnected.`,
-      );
-      this.targets.delete(serialNumber);
-      this.onTargetChange();
-    });
-  }
-
-  private checkDeviceValidity(device: USBDevice): DeviceValidity {
-    const deviceValidity: DeviceValidity = {isValid: true, issues: []};
-    if (!device.serialNumber) {
-      deviceValidity.issues.push(
-        createDeviceErrorMessage(device, SERIAL_NUMBER_ISSUE),
-      );
-      deviceValidity.isValid = false;
-    }
-    if (!findInterfaceAndEndpoint(device)) {
-      deviceValidity.issues.push(
-        createDeviceErrorMessage(device, ADB_INTERFACE_ISSUE),
-      );
-      deviceValidity.isValid = false;
-    }
-    this.recordingProblems.push(...deviceValidity.issues);
-    return deviceValidity;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
deleted file mode 100644
index 68630ee..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2022 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 {RecordingError} from '../recording_error_handling';
-import {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {
-  EXTENSION_ID,
-  EXTENSION_NOT_INSTALLED,
-  isCrOS,
-  isWindows,
-} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {ChromeTarget} from '../targets/chrome_target';
-
-export const CHROME_TARGET_FACTORY = 'ChromeTargetFactory';
-
-export class ChromeTargetFactory implements TargetFactory {
-  readonly kind = CHROME_TARGET_FACTORY;
-  // We only check the connection once at the beginning to:
-  // a) Avoid creating a 'Port' object every time 'getInfo' is called.
-  // b) When a new Port is created, the extension starts communicating with it
-  // and leaves aside the old Port objects, so creating a new Port would break
-  // any ongoing tracing session.
-  isExtensionInstalled: boolean = false;
-  private targets: ChromeTarget[] = [];
-
-  constructor() {
-    this.init();
-  }
-
-  init() {
-    const testPort = chrome.runtime.connect(EXTENSION_ID);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    this.isExtensionInstalled = !!testPort;
-    testPort.disconnect();
-
-    if (!this.isExtensionInstalled) {
-      return;
-    }
-    this.targets.push(new ChromeTarget('Chrome', 'CHROME'));
-    if (isCrOS(navigator.userAgent)) {
-      this.targets.push(new ChromeTarget('ChromeOS', 'CHROME_OS'));
-    }
-    // Pass through the chrome target since it launches ETW on windows through
-    // same path as when we start chrome tracing.
-    if (isWindows(navigator.userAgent)) {
-      this.targets.push(new ChromeTarget('Windows Desktop', 'WINDOWS'));
-    }
-  }
-
-  connectNewTarget(): Promise<RecordingTargetV2> {
-    throw new RecordingError(
-      'Can not create a new Chrome target.' +
-        'All Chrome targets are created at factory initialisation.',
-    );
-  }
-
-  getName(): string {
-    return 'Chrome';
-  }
-
-  listRecordingProblems(): string[] {
-    const recordingProblems = [];
-    if (!this.isExtensionInstalled) {
-      recordingProblems.push(EXTENSION_NOT_INSTALLED);
-    }
-    return recordingProblems;
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    return this.targets;
-  }
-
-  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
-    for (const target of this.targets) {
-      target.onTargetChange = onTargetChange;
-    }
-  }
-}
-
-// We only instantiate the factory if Perfetto UI is open in the Chrome browser.
-// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-if (globalThis.chrome && chrome.runtime) {
-  targetFactoryRegistry.register(new ChromeTargetFactory());
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
deleted file mode 100644
index d238391..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2022 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 {isCrOS, isLinux, isMacOs} from '../recording_utils';
-
-test('parse Chrome on Chrome OS user agent', () => {
-  const userAgent =
-    'Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) ' +
-    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 ' +
-    'Safari/537.36';
-  expect(isCrOS(userAgent)).toBe(true);
-  expect(isMacOs(userAgent)).toBe(false);
-  expect(isLinux(userAgent)).toBe(false);
-});
-
-test('parse Chrome on Mac user agent', () => {
-  const userAgent =
-    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
-    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
-  expect(isCrOS(userAgent)).toBe(false);
-  expect(isMacOs(userAgent)).toBe(true);
-  expect(isLinux(userAgent)).toBe(false);
-});
-
-test('parse Chrome on Linux user agent', () => {
-  const userAgent =
-    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
-    '(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36';
-  expect(isCrOS(userAgent)).toBe(false);
-  expect(isMacOs(userAgent)).toBe(false);
-  expect(isLinux(userAgent)).toBe(true);
-});
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
deleted file mode 100644
index 09e73e7..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2022 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 {RecordingError} from '../recording_error_handling';
-import {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {isLinux, isMacOs} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {HostOsTarget} from '../targets/host_os_target';
-
-export const HOST_OS_TARGET_FACTORY = 'HostOsTargetFactory';
-
-export class HostOsTargetFactory implements TargetFactory {
-  readonly kind = HOST_OS_TARGET_FACTORY;
-  private target?: HostOsTarget;
-  private onTargetChange: OnTargetChangeCallback = () => {};
-
-  connectNewTarget(): Promise<RecordingTargetV2> {
-    throw new RecordingError(
-      'Can not create a new Host OS target.' +
-        'The Host OS target is created at factory initialisation.',
-    );
-  }
-
-  getName(): string {
-    return 'HostOs';
-  }
-
-  listRecordingProblems(): string[] {
-    return [];
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    if (this.target) {
-      return [this.target];
-    }
-    return [];
-  }
-
-  tryEstablishWebsocket(websocketUrl: string) {
-    if (this.target) {
-      if (this.target.getUrl() === websocketUrl) {
-        return;
-      } else {
-        this.target.disconnect();
-      }
-    }
-    this.target = new HostOsTarget(
-      websocketUrl,
-      this.maybeClearTarget.bind(this),
-      this.onTargetChange,
-    );
-    this.onTargetChange();
-  }
-
-  maybeClearTarget(target: HostOsTarget): void {
-    if (this.target === target) {
-      this.target = undefined;
-      this.onTargetChange();
-    }
-  }
-
-  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
-    this.onTargetChange = onTargetChange;
-  }
-}
-
-// We instantiate the host target factory only on Mac, Linux, and Windows.
-if (isMacOs(navigator.userAgent) || isLinux(navigator.userAgent)) {
-  targetFactoryRegistry.register(new HostOsTargetFactory());
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
deleted file mode 100644
index 3c1e3af..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (C) 2022 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 './android_webusb_target_factory';
-import './android_websocket_target_factory';
-import './chrome_target_factory';
-import './host_os_target_factory';
-import './virtual_target_factory';
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
deleted file mode 100644
index a6b98ca..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2022 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 {RecordingError} from '../recording_error_handling';
-import {
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetFactory,
-} from '../recording_interfaces_v2';
-import {targetFactoryRegistry} from '../target_factory_registry';
-import {AndroidVirtualTarget} from '../targets/android_virtual_target';
-
-const VIRTUAL_TARGET_FACTORY = 'VirtualTargetFactory';
-
-export class VirtualTargetFactory implements TargetFactory {
-  readonly kind: string = VIRTUAL_TARGET_FACTORY;
-  private targets: AndroidVirtualTarget[];
-
-  constructor() {
-    this.targets = [];
-    this.targets.push(new AndroidVirtualTarget('Android Q', 29));
-    this.targets.push(new AndroidVirtualTarget('Android P', 28));
-    this.targets.push(new AndroidVirtualTarget('Android O-', 27));
-  }
-
-  connectNewTarget(): Promise<RecordingTargetV2> {
-    throw new RecordingError(
-      'Can not create a new virtual target.' +
-        'All virtual targets are created at factory initialisation.',
-    );
-  }
-
-  getName(): string {
-    return 'Virtual';
-  }
-
-  listRecordingProblems(): string[] {
-    return [];
-  }
-
-  listTargets(): RecordingTargetV2[] {
-    return this.targets;
-  }
-
-  // Virtual targets won't change.
-  setOnTargetChange(_: OnTargetChangeCallback): void {}
-}
-
-targetFactoryRegistry.register(new VirtualTargetFactory());
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
deleted file mode 100644
index 0bac1e4..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (C) 2022 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 {fetchWithTimeout} from '../../../../base/http_utils';
-import {exists} from '../../../../base/utils';
-import {VERSION} from '../../../../gen/perfetto_version';
-import {AdbConnectionImpl} from '../adb_connection_impl';
-import {
-  DataSource,
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TargetInfo,
-  TracingSession,
-  TracingSessionListener,
-} from '../recording_interfaces_v2';
-import {
-  CUSTOM_TRACED_CONSUMER_SOCKET_PATH,
-  DEFAULT_TRACED_CONSUMER_SOCKET_PATH,
-  TRACEBOX_DEVICE_PATH,
-  TRACEBOX_FETCH_TIMEOUT,
-} from '../recording_utils';
-import {TracedTracingSession} from '../traced_tracing_session';
-
-export abstract class AndroidTarget implements RecordingTargetV2 {
-  private consumerSocketPath = DEFAULT_TRACED_CONSUMER_SOCKET_PATH;
-  protected androidApiLevel?: number;
-  protected dataSources?: DataSource[];
-
-  protected constructor(
-    private adbConnection: AdbConnectionImpl,
-    private onTargetChange: OnTargetChangeCallback,
-  ) {}
-
-  abstract getInfo(): TargetInfo;
-
-  // This is called when a usb USBConnectionEvent of type 'disconnect' event is
-  // emitted. This event is emitted when the USB connection is lost (example:
-  // when the user unplugged the connecting cable).
-  async disconnect(disconnectMessage?: string): Promise<void> {
-    await this.adbConnection.disconnect(disconnectMessage);
-  }
-
-  // Starts a tracing session in order to fetch information such as apiLevel
-  // and dataSources from the device. Then, it cancels the session.
-  async fetchTargetInfo(listener: TracingSessionListener): Promise<void> {
-    const tracingSession = await this.createTracingSession(listener);
-    tracingSession.cancel();
-  }
-
-  // We do not support long tracing on Android.
-  canCreateTracingSession(recordingMode: string): boolean {
-    return recordingMode !== 'LONG_TRACE';
-  }
-
-  async createTracingSession(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<TracingSession> {
-    this.adbConnection.onStatus = tracingSessionListener.onStatus;
-    this.adbConnection.onDisconnect = tracingSessionListener.onDisconnect;
-
-    if (!exists(this.androidApiLevel)) {
-      // 1. Fetch the API version from the device.
-      const version = await this.adbConnection.shellAndGetOutput(
-        'getprop ro.build.version.sdk',
-      );
-      this.androidApiLevel = Number(version);
-
-      this.onTargetChange();
-
-      // 2. For older OS versions we push the tracebox binary.
-      if (this.androidApiLevel < 29) {
-        await this.pushTracebox();
-        this.consumerSocketPath = CUSTOM_TRACED_CONSUMER_SOCKET_PATH;
-
-        await this.adbConnection.shellAndWaitCompletion(
-          this.composeTraceboxCommand('traced'),
-        );
-        await this.adbConnection.shellAndWaitCompletion(
-          this.composeTraceboxCommand('traced_probes'),
-        );
-      }
-    }
-
-    const adbStream = await this.adbConnection.connectSocket(
-      this.consumerSocketPath,
-    );
-
-    // 3. Start a tracing session.
-    const tracingSession = new TracedTracingSession(
-      adbStream,
-      tracingSessionListener,
-    );
-    await tracingSession.initConnection();
-
-    if (!this.dataSources) {
-      // 4. Fetch dataSources from QueryServiceState.
-      this.dataSources = await tracingSession.queryServiceState();
-
-      this.onTargetChange();
-    }
-    return tracingSession;
-  }
-
-  async pushTracebox() {
-    const arch = await this.fetchArchitecture();
-    const shortVersion = VERSION.split('-')[0];
-    const requestUrl = `https://commondatastorage.googleapis.com/perfetto-luci-artifacts/${shortVersion}/${arch}/tracebox`;
-    const fetchResponse = await fetchWithTimeout(
-      requestUrl,
-      {method: 'get'},
-      TRACEBOX_FETCH_TIMEOUT,
-    );
-    const traceboxBin = await fetchResponse.arrayBuffer();
-    await this.adbConnection.push(
-      new Uint8Array(traceboxBin),
-      TRACEBOX_DEVICE_PATH,
-    );
-
-    // We explicitly set the tracebox permissions because adb does not reliably
-    // set permissions when uploading the binary.
-    await this.adbConnection.shellAndWaitCompletion(
-      `chmod 755 ${TRACEBOX_DEVICE_PATH}`,
-    );
-  }
-
-  async fetchArchitecture() {
-    const abiList = await this.adbConnection.shellAndGetOutput(
-      'getprop ro.vendor.product.cpu.abilist',
-    );
-    // If multiple ABIs are allowed, the 64bit ones should have higher priority.
-    if (abiList.includes('arm64-v8a')) {
-      return 'android-arm64';
-    } else if (abiList.includes('x86')) {
-      return 'android-x86';
-    } else if (abiList.includes('armeabi-v7a') || abiList.includes('armeabi')) {
-      return 'android-arm';
-    } else if (abiList.includes('x86_64')) {
-      return 'android-x64';
-    }
-    // Most devices have arm64 architectures, so we should return this if
-    // nothing else is found.
-    return 'android-arm64';
-  }
-
-  canConnectWithoutContention(): Promise<boolean> {
-    return this.adbConnection.canConnectWithoutContention();
-  }
-
-  composeTraceboxCommand(applet: string) {
-    // 1. Set the consumer socket.
-    return (
-      'PERFETTO_CONSUMER_SOCK_NAME=@traced_consumer ' +
-      // 2. Set the producer socket.
-      'PERFETTO_PRODUCER_SOCK_NAME=@traced_producer ' +
-      // 3. Start the applet in the background.
-      `/data/local/tmp/tracebox ${applet} --background`
-    );
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
deleted file mode 100644
index d18d075..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2022 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 {RecordingError} from '../recording_error_handling';
-import {
-  RecordingTargetV2,
-  TargetInfo,
-  TracingSession,
-  TracingSessionListener,
-} from '../recording_interfaces_v2';
-
-export class AndroidVirtualTarget implements RecordingTargetV2 {
-  constructor(
-    private name: string,
-    private androidApiLevel: number,
-  ) {}
-
-  canConnectWithoutContention(): Promise<boolean> {
-    return Promise.resolve(true);
-  }
-
-  canCreateTracingSession(): boolean {
-    return false;
-  }
-
-  createTracingSession(_: TracingSessionListener): Promise<TracingSession> {
-    throw new RecordingError(
-      'Can not create tracing session for a virtual target',
-    );
-  }
-
-  disconnect(_?: string): Promise<void> {
-    throw new RecordingError('Can not disconnect from a virtual target');
-  }
-
-  fetchTargetInfo(_: TracingSessionListener): Promise<void> {
-    return Promise.resolve();
-  }
-
-  getInfo(): TargetInfo {
-    return {
-      name: this.name,
-      androidApiLevel: this.androidApiLevel,
-      targetType: 'ANDROID',
-      dataSources: [],
-    };
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
deleted file mode 100644
index 8959720..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2022 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 {AdbConnectionOverWebsocket} from '../adb_connection_over_websocket';
-import {OnTargetChangeCallback, TargetInfo} from '../recording_interfaces_v2';
-import {AndroidTarget} from './android_target';
-
-export class AndroidWebsocketTarget extends AndroidTarget {
-  constructor(
-    private serialNumber: string,
-    websocketUrl: string,
-    onTargetChange: OnTargetChangeCallback,
-  ) {
-    super(
-      new AdbConnectionOverWebsocket(serialNumber, websocketUrl),
-      onTargetChange,
-    );
-  }
-
-  getInfo(): TargetInfo {
-    return {
-      targetType: 'ANDROID',
-      // 'androidApiLevel' will be populated after ADB authorization.
-      androidApiLevel: this.androidApiLevel,
-      dataSources: this.dataSources || [],
-      name: this.serialNumber + ' WebSocket',
-    };
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
deleted file mode 100644
index dc6e64d..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2022 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 {assertExists} from '../../../../base/logging';
-import {AdbConnectionOverWebusb} from '../adb_connection_over_webusb';
-import {AdbKeyManager} from '../auth/adb_key_manager';
-import {OnTargetChangeCallback, TargetInfo} from '../recording_interfaces_v2';
-import {AndroidTarget} from './android_target';
-
-export class AndroidWebusbTarget extends AndroidTarget {
-  constructor(
-    private device: USBDevice,
-    keyManager: AdbKeyManager,
-    onTargetChange: OnTargetChangeCallback,
-  ) {
-    super(new AdbConnectionOverWebusb(device, keyManager), onTargetChange);
-  }
-
-  getInfo(): TargetInfo {
-    const name =
-      assertExists(this.device.productName) +
-      ' ' +
-      assertExists(this.device.serialNumber) +
-      ' WebUsb';
-    return {
-      targetType: 'ANDROID',
-      // 'androidApiLevel' will be populated after ADB authorization.
-      androidApiLevel: this.androidApiLevel,
-      dataSources: this.dataSources || [],
-      name,
-    };
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
deleted file mode 100644
index 9baaf96..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2022 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 {ChromeTracedTracingSession} from '../chrome_traced_tracing_session';
-import {
-  ChromeTargetInfo,
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TracingSession,
-  TracingSessionListener,
-} from '../recording_interfaces_v2';
-
-export class ChromeTarget implements RecordingTargetV2 {
-  onTargetChange?: OnTargetChangeCallback;
-  private chromeCategories?: string[];
-
-  constructor(
-    private name: string,
-    private targetType: 'CHROME' | 'CHROME_OS' | 'WINDOWS',
-  ) {}
-
-  getInfo(): ChromeTargetInfo {
-    return {
-      targetType: this.targetType,
-      name: this.name,
-      dataSources: [
-        {name: 'chromeCategories', descriptor: this.chromeCategories},
-      ],
-    };
-  }
-
-  // Chrome targets are created after we check that the extension is installed,
-  // so they support tracing sessions.
-  canCreateTracingSession(): boolean {
-    return true;
-  }
-
-  async createTracingSession(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<TracingSession> {
-    const tracingSession = new ChromeTracedTracingSession(
-      tracingSessionListener,
-    );
-    tracingSession.initConnection();
-
-    if (!this.chromeCategories) {
-      // Fetch chrome categories from the extension.
-      this.chromeCategories = await tracingSession.getCategories();
-      if (this.onTargetChange) {
-        this.onTargetChange();
-      }
-    }
-
-    return tracingSession;
-  }
-
-  // Starts a tracing session in order to fetch chrome categories from the
-  // device. Then, it cancels the session.
-  async fetchTargetInfo(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<void> {
-    const tracingSession = await this.createTracingSession(
-      tracingSessionListener,
-    );
-    tracingSession.cancel();
-  }
-
-  disconnect(_disconnectMessage?: string): Promise<void> {
-    return Promise.resolve(undefined);
-  }
-
-  // We can connect to the Chrome target without taking the connection away
-  // from another process.
-  async canConnectWithoutContention(): Promise<boolean> {
-    return true;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
deleted file mode 100644
index 7b32fcc..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-// Copyright (C) 2022 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 {HostOsByteStream} from '../host_os_byte_stream';
-import {RecordingError} from '../recording_error_handling';
-import {
-  DataSource,
-  HostOsTargetInfo,
-  OnDisconnectCallback,
-  OnTargetChangeCallback,
-  RecordingTargetV2,
-  TracingSession,
-  TracingSessionListener,
-} from '../recording_interfaces_v2';
-import {
-  isLinux,
-  isMacOs,
-  WEBSOCKET_CLOSED_ABNORMALLY_CODE,
-} from '../recording_utils';
-import {TracedTracingSession} from '../traced_tracing_session';
-
-export class HostOsTarget implements RecordingTargetV2 {
-  private readonly targetType: 'LINUX' | 'MACOS';
-  private readonly name: string;
-  private websocket: WebSocket;
-  private streams = new Set<HostOsByteStream>();
-  private dataSources?: DataSource[];
-  private onDisconnect: OnDisconnectCallback = (_) => {};
-
-  constructor(
-    websocketUrl: string,
-    private maybeClearTarget: (target: HostOsTarget) => void,
-    private onTargetChange: OnTargetChangeCallback,
-  ) {
-    if (isMacOs(navigator.userAgent)) {
-      this.name = 'MacOS';
-      this.targetType = 'MACOS';
-    } else if (isLinux(navigator.userAgent)) {
-      this.name = 'Linux';
-      this.targetType = 'LINUX';
-    } else {
-      throw new RecordingError(
-        'Host OS target created on an unsupported operating system.',
-      );
-    }
-
-    this.websocket = new WebSocket(websocketUrl);
-    this.websocket.onclose = this.onClose.bind(this);
-    // 'onError' gets called when the websocketURL where the UI tries to connect
-    // is disallowed by the Content Security Policy. In this case, we disconnect
-    // the target.
-    this.websocket.onerror = this.disconnect.bind(this);
-  }
-
-  getInfo(): HostOsTargetInfo {
-    return {
-      targetType: this.targetType,
-      name: this.name,
-      dataSources: this.dataSources || [],
-    };
-  }
-
-  canCreateTracingSession(): boolean {
-    return true;
-  }
-
-  async createTracingSession(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<TracingSession> {
-    this.onDisconnect = tracingSessionListener.onDisconnect;
-
-    const osStream = await HostOsByteStream.create(this.getUrl());
-    this.streams.add(osStream);
-    const tracingSession = new TracedTracingSession(
-      osStream,
-      tracingSessionListener,
-    );
-    await tracingSession.initConnection();
-
-    if (!this.dataSources) {
-      this.dataSources = await tracingSession.queryServiceState();
-      this.onTargetChange();
-    }
-    return tracingSession;
-  }
-
-  // Starts a tracing session in order to fetch data sources from the
-  // device. Then, it cancels the session.
-  async fetchTargetInfo(
-    tracingSessionListener: TracingSessionListener,
-  ): Promise<void> {
-    const tracingSession = await this.createTracingSession(
-      tracingSessionListener,
-    );
-    tracingSession.cancel();
-  }
-
-  async disconnect(): Promise<void> {
-    if (this.websocket.readyState === this.websocket.OPEN) {
-      this.websocket.close();
-      // We remove the 'onclose' callback so the 'disconnect' method doesn't get
-      // executed twice.
-      this.websocket.onclose = null;
-    }
-    for (const stream of this.streams) {
-      stream.close();
-    }
-    // We remove the existing target from the factory if present.
-    this.maybeClearTarget(this);
-    // We run the onDisconnect callback in case this target is used for tracing.
-    this.onDisconnect();
-  }
-
-  // We can connect to the Host OS without taking the connection away from
-  // another process.
-  async canConnectWithoutContention(): Promise<boolean> {
-    return true;
-  }
-
-  getUrl() {
-    return this.websocket.url;
-  }
-
-  private onClose(ev: CloseEvent): void {
-    if (ev.code === WEBSOCKET_CLOSED_ABNORMALLY_CODE) {
-      console.info(
-        `It's safe to ignore the 'WebSocket connection to ${this.getUrl()} error above, if present. It occurs when ` +
-          'checking the connection to the local Websocket server.',
-      );
-    }
-    this.disconnect();
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
deleted file mode 100644
index 33151bd..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
+++ /dev/null
@@ -1,439 +0,0 @@
-// Copyright (C) 2022 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 protobuf from 'protobufjs/minimal';
-import {defer, Deferred} from '../../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
-import {
-  DisableTracingRequest,
-  DisableTracingResponse,
-  EnableTracingRequest,
-  EnableTracingResponse,
-  FreeBuffersRequest,
-  FreeBuffersResponse,
-  GetTraceStatsRequest,
-  GetTraceStatsResponse,
-  IBufferStats,
-  IMethodInfo,
-  IPCFrame,
-  ISlice,
-  QueryServiceStateRequest,
-  QueryServiceStateResponse,
-  ReadBuffersRequest,
-  ReadBuffersResponse,
-  TraceConfig,
-} from '../protos';
-import {RecordingError} from './recording_error_handling';
-import {
-  ByteStream,
-  DataSource,
-  TracingSession,
-  TracingSessionListener,
-} from './recording_interfaces_v2';
-import {
-  BUFFER_USAGE_INCORRECT_FORMAT,
-  BUFFER_USAGE_NOT_ACCESSIBLE,
-  PARSING_UNABLE_TO_DECODE_METHOD,
-  PARSING_UNKNWON_REQUEST_ID,
-  PARSING_UNRECOGNIZED_MESSAGE,
-  PARSING_UNRECOGNIZED_PORT,
-  RECORDING_IN_PROGRESS,
-} from './recording_utils';
-import {exists} from '../../../base/utils';
-
-// See wire_protocol.proto for more details.
-const WIRE_PROTOCOL_HEADER_SIZE = 4;
-// See basic_types.h (kIPCBufferSize) for more details.
-const MAX_IPC_BUFFER_SIZE = 128 * 1024;
-
-const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
-const TRACE_PACKET_PROTO_ID = 1;
-const TRACE_PACKET_PROTO_TAG =
-  (TRACE_PACKET_PROTO_ID << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
-
-function parseMessageSize(buffer: Uint8Array) {
-  const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.length);
-  return dv.getUint32(0, true);
-}
-
-// This class implements the protocol described in
-// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
-export class TracedTracingSession implements TracingSession {
-  // Buffers received wire protocol data.
-  private incomingBuffer = new Uint8Array(MAX_IPC_BUFFER_SIZE);
-  private bufferedPartLength = 0;
-  private currentFrameLength?: number;
-
-  private availableMethods: IMethodInfo[] = [];
-  private serviceId = -1;
-
-  private resolveBindingPromise!: Deferred<void>;
-  private requestMethods = new Map<number, string>();
-
-  // Needed for ReadBufferResponse: all the trace packets are split into
-  // several slices. |partialPacket| is the buffer for them. Once we receive a
-  // slice with the flag |lastSliceForPacket|, a new packet is created.
-  private partialPacket: ISlice[] = [];
-  // Accumulates trace packets into a proto trace file..
-  private traceProtoWriter = protobuf.Writer.create();
-
-  // Accumulates DataSource objects from QueryServiceStateResponse,
-  // which can have >1 replies for each query
-  // go/codesearch/android/external/perfetto/protos/
-  // perfetto/ipc/consumer_port.proto;l=243-246
-  private pendingDataSources: DataSource[] = [];
-
-  // For concurrent calls to 'QueryServiceState', we return the same value.
-  private pendingQssMessage?: Deferred<DataSource[]>;
-
-  // Wire protocol request ID. After each request it is increased. It is needed
-  // to keep track of the type of request, and parse the response correctly.
-  private requestId = 1;
-
-  private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
-
-  // The bytestream is obtained when creating a connection with a target.
-  // For instance, the AdbStream is obtained from a connection with an Adb
-  // device.
-  constructor(
-    private byteStream: ByteStream,
-    private tracingSessionListener: TracingSessionListener,
-  ) {
-    this.byteStream.addOnStreamDataCallback((data) =>
-      this.handleReceivedData(data),
-    );
-    this.byteStream.addOnStreamCloseCallback(() => this.clearState());
-  }
-
-  queryServiceState(): Promise<DataSource[]> {
-    if (this.pendingQssMessage) {
-      return this.pendingQssMessage;
-    }
-
-    const requestProto = QueryServiceStateRequest.encode(
-      new QueryServiceStateRequest(),
-    ).finish();
-    this.rpcInvoke('QueryServiceState', requestProto);
-
-    return (this.pendingQssMessage = defer<DataSource[]>());
-  }
-
-  start(config: TraceConfig): void {
-    const duration = config.durationMs;
-    this.tracingSessionListener.onStatus(
-      `${RECORDING_IN_PROGRESS}${
-        duration ? ' for ' + duration.toString() + ' ms' : ''
-      }...`,
-    );
-
-    const enableTracingRequest = new EnableTracingRequest();
-    enableTracingRequest.traceConfig = config;
-    const enableTracingRequestProto =
-      EnableTracingRequest.encode(enableTracingRequest).finish();
-    this.rpcInvoke('EnableTracing', enableTracingRequestProto);
-  }
-
-  cancel(): void {
-    this.terminateConnection();
-  }
-
-  stop(): void {
-    const requestProto = DisableTracingRequest.encode(
-      new DisableTracingRequest(),
-    ).finish();
-    this.rpcInvoke('DisableTracing', requestProto);
-  }
-
-  async getTraceBufferUsage(): Promise<number> {
-    if (!this.byteStream.isConnected()) {
-      // TODO(octaviant): make this more in line with the other trace buffer
-      //  error cases.
-      return 0;
-    }
-    const bufferStats = await this.getBufferStats();
-    let percentageUsed = -1;
-    for (const buffer of bufferStats) {
-      if (
-        !Number.isFinite(buffer.bytesWritten) ||
-        !Number.isFinite(buffer.bufferSize)
-      ) {
-        continue;
-      }
-      const used = assertExists(buffer.bytesWritten);
-      const total = assertExists(buffer.bufferSize);
-      if (total >= 0) {
-        percentageUsed = Math.max(percentageUsed, used / total);
-      }
-    }
-
-    if (percentageUsed === -1) {
-      return Promise.reject(new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT));
-    }
-    return percentageUsed;
-  }
-
-  initConnection(): Promise<void> {
-    // bind IPC methods
-    const requestId = this.requestId++;
-    const frame = new IPCFrame({
-      requestId,
-      msgBindService: new IPCFrame.BindService({serviceName: 'ConsumerPort'}),
-    });
-    this.writeFrame(frame);
-
-    // We shouldn't bind multiple times to the service in the same tracing
-    // session.
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    assertFalse(!!this.resolveBindingPromise);
-    this.resolveBindingPromise = defer<void>();
-    return this.resolveBindingPromise;
-  }
-
-  private getBufferStats(): Promise<IBufferStats[]> {
-    const getTraceStatsRequestProto = GetTraceStatsRequest.encode(
-      new GetTraceStatsRequest(),
-    ).finish();
-    try {
-      this.rpcInvoke('GetTraceStats', getTraceStatsRequestProto);
-    } catch (e) {
-      // GetTraceStats was introduced only on Android 10.
-      this.raiseError(e);
-    }
-
-    const statsMessage = defer<IBufferStats[]>();
-    this.pendingStatsMessages.push(statsMessage);
-    return statsMessage;
-  }
-
-  private terminateConnection(): void {
-    this.clearState();
-    const requestProto = FreeBuffersRequest.encode(
-      new FreeBuffersRequest(),
-    ).finish();
-    this.rpcInvoke('FreeBuffers', requestProto);
-    this.byteStream.close();
-  }
-
-  private clearState() {
-    for (const statsMessage of this.pendingStatsMessages) {
-      statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
-    }
-    this.pendingStatsMessages = [];
-    this.pendingDataSources = [];
-    this.pendingQssMessage = undefined;
-  }
-
-  private rpcInvoke(methodName: string, argsProto: Uint8Array): void {
-    if (!this.byteStream.isConnected()) {
-      return;
-    }
-    const method = this.availableMethods.find((m) => m.name === methodName);
-    if (!exists(method) || !exists(method.id)) {
-      throw new RecordingError(
-        `Method ${methodName} not supported by the target`,
-      );
-    }
-    const requestId = this.requestId++;
-    const frame = new IPCFrame({
-      requestId,
-      msgInvokeMethod: new IPCFrame.InvokeMethod({
-        serviceId: this.serviceId,
-        methodId: method.id,
-        argsProto,
-      }),
-    });
-    this.requestMethods.set(requestId, methodName);
-    this.writeFrame(frame);
-  }
-
-  private writeFrame(frame: IPCFrame): void {
-    const frameProto: Uint8Array = IPCFrame.encode(frame).finish();
-    const frameLen = frameProto.length;
-    const buf = new Uint8Array(WIRE_PROTOCOL_HEADER_SIZE + frameLen);
-    const dv = new DataView(buf.buffer);
-    dv.setUint32(0, frameProto.length, /* littleEndian */ true);
-    for (let i = 0; i < frameLen; i++) {
-      dv.setUint8(WIRE_PROTOCOL_HEADER_SIZE + i, frameProto[i]);
-    }
-    this.byteStream.write(buf);
-  }
-
-  private handleReceivedData(rawData: Uint8Array): void {
-    // we parse the length of the next frame if it's available
-    if (
-      this.currentFrameLength === undefined &&
-      this.canCompleteLengthHeader(rawData)
-    ) {
-      const remainingFrameBytes =
-        WIRE_PROTOCOL_HEADER_SIZE - this.bufferedPartLength;
-      this.appendToIncomingBuffer(rawData.subarray(0, remainingFrameBytes));
-      rawData = rawData.subarray(remainingFrameBytes);
-
-      this.currentFrameLength = parseMessageSize(this.incomingBuffer);
-      this.bufferedPartLength = 0;
-    }
-
-    // Parse all complete frames.
-    while (
-      this.currentFrameLength !== undefined &&
-      this.bufferedPartLength + rawData.length >= this.currentFrameLength
-    ) {
-      // Read the remaining part of this message.
-      const bytesToCompleteMessage =
-        this.currentFrameLength - this.bufferedPartLength;
-      this.appendToIncomingBuffer(rawData.subarray(0, bytesToCompleteMessage));
-      this.parseFrame(this.incomingBuffer.subarray(0, this.currentFrameLength));
-      this.bufferedPartLength = 0;
-      // Remove the data just parsed.
-      rawData = rawData.subarray(bytesToCompleteMessage);
-
-      if (!this.canCompleteLengthHeader(rawData)) {
-        this.currentFrameLength = undefined;
-        break;
-      }
-      this.currentFrameLength = parseMessageSize(rawData);
-      rawData = rawData.subarray(WIRE_PROTOCOL_HEADER_SIZE);
-    }
-
-    // Buffer the remaining data (part of the next message).
-    this.appendToIncomingBuffer(rawData);
-  }
-
-  private canCompleteLengthHeader(newData: Uint8Array): boolean {
-    return newData.length + this.bufferedPartLength > WIRE_PROTOCOL_HEADER_SIZE;
-  }
-
-  private appendToIncomingBuffer(array: Uint8Array): void {
-    this.incomingBuffer.set(array, this.bufferedPartLength);
-    this.bufferedPartLength += array.length;
-  }
-
-  private parseFrame(frameBuffer: Uint8Array): void {
-    // Get a copy of the ArrayBuffer to avoid the original being overriden.
-    // See 170256902#comment21
-    const frame = IPCFrame.decode(frameBuffer.slice());
-    if (frame.msg === 'msgBindServiceReply') {
-      const msgBindServiceReply = frame.msgBindServiceReply;
-      if (
-        exists(msgBindServiceReply) &&
-        exists(msgBindServiceReply.methods) &&
-        exists(msgBindServiceReply.serviceId)
-      ) {
-        assertTrue(msgBindServiceReply.success === true);
-        this.availableMethods = msgBindServiceReply.methods;
-        this.serviceId = msgBindServiceReply.serviceId;
-        this.resolveBindingPromise.resolve();
-      }
-    } else if (frame.msg === 'msgInvokeMethodReply') {
-      const msgInvokeMethodReply = frame.msgInvokeMethodReply;
-      // We process messages without a `replyProto` field (for instance
-      // `FreeBuffers` does not have `replyProto`). However, we ignore messages
-      // without a valid 'success' field.
-      if (msgInvokeMethodReply?.success !== true) {
-        return;
-      }
-
-      const method = this.requestMethods.get(frame.requestId);
-      if (!method) {
-        this.raiseError(`${PARSING_UNKNWON_REQUEST_ID}: ${frame.requestId}`);
-        return;
-      }
-      const decoder = decoders.get(method);
-      if (decoder === undefined) {
-        this.raiseError(`${PARSING_UNABLE_TO_DECODE_METHOD}: ${method}`);
-        return;
-      }
-      const data = {...decoder(msgInvokeMethodReply.replyProto)};
-
-      if (method === 'ReadBuffers') {
-        for (const slice of data.slices ?? []) {
-          this.partialPacket.push(slice);
-          if (slice.lastSliceForPacket === true) {
-            let bufferSize = 0;
-            for (const slice of this.partialPacket) {
-              bufferSize += slice.data!.length;
-            }
-            const tracePacket = new Uint8Array(bufferSize);
-            let written = 0;
-            for (const slice of this.partialPacket) {
-              const data = slice.data!;
-              tracePacket.set(data, written);
-              written += data.length;
-            }
-            this.traceProtoWriter.uint32(TRACE_PACKET_PROTO_TAG);
-            this.traceProtoWriter.bytes(tracePacket);
-            this.partialPacket = [];
-          }
-        }
-        if (msgInvokeMethodReply.hasMore === false) {
-          this.tracingSessionListener.onTraceData(
-            this.traceProtoWriter.finish(),
-          );
-          this.terminateConnection();
-        }
-      } else if (method === 'EnableTracing') {
-        const readBuffersRequestProto = ReadBuffersRequest.encode(
-          new ReadBuffersRequest(),
-        ).finish();
-        this.rpcInvoke('ReadBuffers', readBuffersRequestProto);
-      } else if (method === 'GetTraceStats') {
-        const maybePendingStatsMessage = this.pendingStatsMessages.shift();
-        if (maybePendingStatsMessage) {
-          maybePendingStatsMessage.resolve(data?.traceStats?.bufferStats ?? []);
-        }
-      } else if (method === 'FreeBuffers') {
-        // No action required. If we successfully read a whole trace,
-        // we close the connection. Alternatively, if the tracing finishes
-        // with an exception or if the user cancels it, we also close the
-        // connection.
-      } else if (method === 'DisableTracing') {
-        // No action required. Same reasoning as for FreeBuffers.
-      } else if (method === 'QueryServiceState') {
-        const dataSources =
-          (data as QueryServiceStateResponse)?.serviceState?.dataSources || [];
-        for (const dataSource of dataSources) {
-          const name = dataSource?.dsDescriptor?.name;
-          if (name) {
-            this.pendingDataSources.push({
-              name,
-              descriptor: dataSource.dsDescriptor,
-            });
-          }
-        }
-        if (msgInvokeMethodReply.hasMore === false) {
-          assertExists(this.pendingQssMessage).resolve(this.pendingDataSources);
-          this.pendingDataSources = [];
-          this.pendingQssMessage = undefined;
-        }
-      } else {
-        this.raiseError(`${PARSING_UNRECOGNIZED_PORT}: ${method}`);
-      }
-    } else {
-      this.raiseError(`${PARSING_UNRECOGNIZED_MESSAGE}: ${frame.msg}`);
-    }
-  }
-
-  private raiseError(message: string): void {
-    this.terminateConnection();
-    this.tracingSessionListener.onError(message);
-  }
-}
-
-const decoders = new Map<string, Function>()
-  .set('EnableTracing', EnableTracingResponse.decode)
-  .set('FreeBuffers', FreeBuffersResponse.decode)
-  .set('ReadBuffers', ReadBuffersResponse.decode)
-  .set('DisableTracing', DisableTracingResponse.decode)
-  .set('GetTraceStats', GetTraceStatsResponse.decode)
-  .set('QueryServiceState', QueryServiceStateResponse.decode);
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
deleted file mode 100644
index 2da8f5b..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2022 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 {
-  ADB_ENDPOINT,
-  DEFAULT_WEBSOCKET_URL,
-  TRACED_ENDPOINT,
-} from '../recording_ui_utils';
-import {TargetFactory} from './recording_interfaces_v2';
-import {
-  ANDROID_WEBSOCKET_TARGET_FACTORY,
-  AndroidWebsocketTargetFactory,
-} from './target_factories/android_websocket_target_factory';
-import {
-  HOST_OS_TARGET_FACTORY,
-  HostOsTargetFactory,
-} from './target_factories/host_os_target_factory';
-import {targetFactoryRegistry} from './target_factory_registry';
-
-// The WebsocketMenuController will handle paths for all factories which
-// connect over websocket. At present, these are:
-// - adb websocket factory
-// - host OS websocket factory
-export class WebsocketMenuController {
-  private path: string = DEFAULT_WEBSOCKET_URL;
-
-  getPath(): string {
-    return this.path;
-  }
-
-  setPath(path: string): void {
-    this.path = path;
-  }
-
-  onPathChange(): void {
-    if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
-      const androidTargetFactory = targetFactoryRegistry.get(
-        ANDROID_WEBSOCKET_TARGET_FACTORY,
-      ) as AndroidWebsocketTargetFactory;
-      androidTargetFactory.tryEstablishWebsocket(this.path + ADB_ENDPOINT);
-    }
-
-    if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
-      const hostTargetFactory = targetFactoryRegistry.get(
-        HOST_OS_TARGET_FACTORY,
-      ) as HostOsTargetFactory;
-      hostTargetFactory.tryEstablishWebsocket(this.path + TRACED_ENDPOINT);
-    }
-  }
-
-  getTargetFactories(): TargetFactory[] {
-    const targetFactories = [];
-    if (targetFactoryRegistry.has(ANDROID_WEBSOCKET_TARGET_FACTORY)) {
-      targetFactories.push(
-        targetFactoryRegistry.get(ANDROID_WEBSOCKET_TARGET_FACTORY),
-      );
-    }
-    if (targetFactoryRegistry.has(HOST_OS_TARGET_FACTORY)) {
-      targetFactories.push(targetFactoryRegistry.get(HOST_OS_TARGET_FACTORY));
-    }
-    return targetFactories;
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
index be29691..7504ec9 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -25,12 +25,7 @@
 import {isGetCategoriesResponse} from './chrome_proxy_record_controller';
 import {RecordConfig, createEmptyRecordConfig} from './record_config_types';
 import {RecordController} from './record_controller';
-import {scheduleFullRedraw} from '../../widgets/raf';
 import {App} from '../../public/app';
-import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
-import {AndroidWebsocketTargetFactory} from './recordingV2/target_factories/android_websocket_target_factory';
-import {AndroidWebusbTargetFactory} from './recordingV2/target_factories/android_webusb_target_factory';
-import {exists} from '../../base/utils';
 
 const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
 
@@ -41,31 +36,22 @@
   private _state: RecordingState = createEmptyState();
   private recCtl: RecordController;
 
-  constructor(app: App, useRecordingV2: boolean) {
+  constructor(app: App) {
     this.app = app;
     const extensionLocalChannel = new MessageChannel();
     this.recCtl = new RecordController(app, this, extensionLocalChannel.port1);
     this.setupExtentionPort(extensionLocalChannel);
 
-    if (useRecordingV2) {
-      targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
-      if (exists(navigator.usb)) {
-        targetFactoryRegistry.register(
-          new AndroidWebusbTargetFactory(navigator.usb),
-        );
-      }
-    } else {
-      this.updateAvailableAdbDevices();
-      try {
-        navigator.usb.addEventListener('connect', () =>
-          this.updateAvailableAdbDevices(),
-        );
-        navigator.usb.addEventListener('disconnect', () =>
-          this.updateAvailableAdbDevices(),
-        );
-      } catch (e) {
-        console.error('WebUSB API not supported');
-      }
+    this.updateAvailableAdbDevices();
+    try {
+      navigator.usb.addEventListener('connect', () =>
+        this.updateAvailableAdbDevices(),
+      );
+      navigator.usb.addEventListener('disconnect', () =>
+        this.updateAvailableAdbDevices(),
+      );
+    } catch (e) {
+      console.error('WebUSB API not supported');
     }
   }
 
@@ -158,7 +144,7 @@
         (message: object, _port: chrome.runtime.Port) => {
           if (isGetCategoriesResponse(message)) {
             this._state.chromeCategories = message.categories;
-            scheduleFullRedraw();
+            this.app.raf.scheduleFullRedraw();
             return;
           }
           extensionLocalChannel.port2.postMessage(message);
@@ -193,7 +179,7 @@
 
     this.setAvailableAdbDevices(availableAdbDevices);
     this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
-    scheduleFullRedraw();
+    this.app.raf.scheduleFullRedraw();
     return availableAdbDevices;
   }
 
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
deleted file mode 100644
index 0e34f5c..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2022 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 {
-  RecordingTargetV2,
-  TargetFactory,
-} from './recordingV2/recording_interfaces_v2';
-import {RecordingPageController} from './recordingV2/recording_page_controller';
-import {RECORDING_MODAL_DIALOG_KEY} from './recordingV2/recording_utils';
-import {closeModal} from '../../widgets/modal';
-
-interface RecordingMultipleChoiceAttrs {
-  targetFactories: TargetFactory[];
-  // Reference to the controller which maintains the state of the recording
-  // page.
-  controller: RecordingPageController;
-}
-
-export class RecordingMultipleChoice
-  implements m.ClassComponent<RecordingMultipleChoiceAttrs>
-{
-  private selectedIndex: number = -1;
-
-  targetSelection(
-    targets: RecordingTargetV2[],
-    controller: RecordingPageController,
-  ): m.Vnode | undefined {
-    const targetInfo = controller.getTargetInfo();
-    const targetNames = [];
-    this.selectedIndex = -1;
-    for (let i = 0; i < targets.length; i++) {
-      const targetName = targets[i].getInfo().name;
-      targetNames.push(m('option', targetName));
-      if (targetInfo && targetName === targetInfo.name) {
-        this.selectedIndex = i;
-      }
-    }
-
-    const selectedIndex = this.selectedIndex;
-    return m(
-      'label',
-      m(
-        'select',
-        {
-          selectedIndex,
-          onchange: (e: Event) => {
-            controller.onTargetSelection((e.target as HTMLSelectElement).value);
-          },
-          onupdate: (select) => {
-            // Work around mithril bug
-            // (https://github.com/MithrilJS/mithril.js/issues/2107): We
-            // may update the select's options while also changing the
-            // selectedIndex at the same time. The update of selectedIndex
-            // may be applied before the new options are added to the
-            // select element. Because the new selectedIndex may be
-            // outside of the select's options at that time, we have to
-            // reselect the correct index here after any new children were
-            // added.
-            (select.dom as HTMLSelectElement).selectedIndex =
-              this.selectedIndex;
-          },
-          ...{size: targets.length, multiple: 'multiple'},
-        },
-        ...targetNames,
-      ),
-    );
-  }
-
-  view({attrs}: m.CVnode<RecordingMultipleChoiceAttrs>): m.Vnode[] | undefined {
-    const controller = attrs.controller;
-    if (!controller.shouldShowTargetSelection()) {
-      return undefined;
-    }
-    const targets: RecordingTargetV2[] = [];
-    for (const targetFactory of attrs.targetFactories) {
-      for (const target of targetFactory.listTargets()) {
-        targets.push(target);
-      }
-    }
-    if (targets.length === 0) {
-      return undefined;
-    }
-
-    return [
-      m('text', 'Select target:'),
-      m(
-        '.record-modal-command',
-        this.targetSelection(targets, controller),
-        m(
-          'button.record-modal-button-high',
-          {
-            disabled: this.selectedIndex === -1,
-            onclick: () => {
-              closeModal(RECORDING_MODAL_DIALOG_KEY);
-              controller.onStartRecordingPressed();
-            },
-          },
-          'Connect',
-        ),
-      ),
-    ];
-  }
-}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
deleted file mode 100644
index 4d3d048..0000000
--- a/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (C) 2022 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 {RecordingPageController} from './recordingV2/recording_page_controller';
-import {
-  EXTENSION_URL,
-  RECORDING_MODAL_DIALOG_KEY,
-} from './recordingV2/recording_utils';
-import {
-  CHROME_TARGET_FACTORY,
-  ChromeTargetFactory,
-} from './recordingV2/target_factories/chrome_target_factory';
-import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
-import {WebsocketMenuController} from './recordingV2/websocket_menu_controller';
-import {closeModal, showModal} from '../../widgets/modal';
-import {CodeSnippet} from './record_widgets';
-import {RecordingMultipleChoice} from './recording_multiple_choice';
-
-const RUN_WEBSOCKET_CMD =
-  '# Get tracebox\n' +
-  'curl -LO https://get.perfetto.dev/tracebox\n' +
-  'chmod +x ./tracebox\n' +
-  '# Option A - trace android devices\n' +
-  'adb start-server\n' +
-  '# Option B - trace the host OS\n' +
-  './tracebox traced --background\n' +
-  './tracebox traced_probes --background\n' +
-  '# Start the websocket server\n' +
-  './tracebox websocket_bridge\n';
-
-export function showAddNewTargetModal(controller: RecordingPageController) {
-  showModal({
-    title: 'Add new recording target',
-    key: RECORDING_MODAL_DIALOG_KEY,
-    content: () =>
-      m(
-        '.record-modal',
-        m('text', 'Select platform:'),
-        assembleWebusbSection(controller),
-        m('.line'),
-        assembleWebsocketSection(controller),
-        m('.line'),
-        assembleChromeSection(controller),
-      ),
-  });
-}
-
-function assembleWebusbSection(
-  recordingPageController: RecordingPageController,
-): m.Vnode {
-  return m(
-    '.record-modal-section',
-    m('.logo-wrapping', m('i.material-icons', 'usb')),
-    m(
-      '.record-modal-description',
-      m('h3', 'Android device over WebUSB'),
-      m(
-        'text',
-        'Android developers: this option cannot co-operate ' +
-          'with the adb host on your machine. Only one entity between ' +
-          'the browser and adb can control the USB endpoint. If adb is ' +
-          'running, you will be prompted to re-assign the device to the ' +
-          'browser. Use the websocket option below to use both ' +
-          'simultaneously.',
-      ),
-      m(
-        '.record-modal-button',
-        {
-          onclick: () => {
-            closeModal(RECORDING_MODAL_DIALOG_KEY);
-            recordingPageController.addAndroidDevice();
-          },
-        },
-        'Connect new WebUSB driver',
-      ),
-    ),
-  );
-}
-
-function assembleWebsocketSection(
-  recordingPageController: RecordingPageController,
-): m.Vnode {
-  const websocketComponents = [];
-  websocketComponents.push(
-    m('h3', 'Android / Linux / MacOS device via Websocket'),
-  );
-  websocketComponents.push(
-    m(
-      'text',
-      'This option assumes that the adb server is already ' +
-        'running on your machine.',
-    ),
-    m(
-      '.record-modal-command',
-      m(CodeSnippet, {
-        text: RUN_WEBSOCKET_CMD,
-      }),
-    ),
-  );
-
-  websocketComponents.push(
-    m(
-      '.record-modal-command',
-      m('text', 'Websocket bridge address: '),
-      m('input[type=text]', {
-        value: websocketMenuController.getPath(),
-        oninput() {
-          websocketMenuController.setPath(this.value);
-        },
-      }),
-      m(
-        '.record-modal-logo-button',
-        {
-          onclick: () => websocketMenuController.onPathChange(),
-        },
-        m('i.material-icons', 'refresh'),
-      ),
-    ),
-  );
-
-  websocketComponents.push(
-    m(RecordingMultipleChoice, {
-      controller: recordingPageController,
-      targetFactories: websocketMenuController.getTargetFactories(),
-    }),
-  );
-
-  return m(
-    '.record-modal-section',
-    m('.logo-wrapping', m('i.material-icons', 'settings_ethernet')),
-    m('.record-modal-description', ...websocketComponents),
-  );
-}
-
-function assembleChromeSection(
-  recordingPageController: RecordingPageController,
-): m.Vnode | undefined {
-  if (!targetFactoryRegistry.has(CHROME_TARGET_FACTORY)) {
-    return undefined;
-  }
-
-  const chromeComponents = [];
-  chromeComponents.push(m('h3', 'Chrome Browser instance or ChromeOS device'));
-
-  const chromeFactory: ChromeTargetFactory = targetFactoryRegistry.get(
-    CHROME_TARGET_FACTORY,
-  ) as ChromeTargetFactory;
-
-  if (!chromeFactory.isExtensionInstalled) {
-    chromeComponents.push(
-      m(
-        'text',
-        'Install the extension ',
-        m('a', {href: EXTENSION_URL, target: '_blank'}, 'from this link '),
-        'and refresh the page.',
-      ),
-    );
-  } else {
-    chromeComponents.push(
-      m(RecordingMultipleChoice, {
-        controller: recordingPageController,
-        targetFactories: [chromeFactory],
-      }),
-    );
-  }
-
-  return m(
-    '.record-modal-section',
-    m('.logo-wrapping', m('i.material-icons', 'web')),
-    m('.record-modal-description', ...chromeComponents),
-  );
-}
-
-const websocketMenuController = new WebsocketMenuController();
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS b/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS
new file mode 100644
index 0000000..ffd5543
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/OWNERS
@@ -0,0 +1 @@
+primiano@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.ts
new file mode 100644
index 0000000..28798d5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_device.ts
@@ -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.
+
+import {defer} from '../../../base/deferred';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {okResult, Result} from '../../../base/result';
+import {utf8Decode} from '../../../base/string_utils';
+import {ByteStream} from '../interfaces/byte_stream';
+
+/**
+ * A base abstraction that represents an Android ADB device, allowing to shell
+ * commands and create streams (e.g. connecting to a UNIX-socket).
+ * This abstraction exists so that AdbTracingSession can drive a tracing session
+ * regardless of the underlying Webusb Websocket connection.
+ * AdbWebusbDevice and AdbWebsocketDevice implement this.
+ * @see @class AdbWebusbDevice
+ * @see @class AdbWebsocketDevice
+ */
+export abstract class AdbDevice {
+  /**
+   * Opens an ADB Stream. Example services:
+   * - 'shell:command arg1 arg2 ...'
+   * - 'shell:' for interactive shell
+   * - 'localfilesystem:/dev/socket/xxx': for UNIX sockets.
+   * - 'localabstract:sock_name': for UNIX abstract sockets.
+   */
+  abstract createStream(svc: string): Promise<Result<ByteStream>>;
+
+  abstract close(): void;
+
+  /** Invoke a command and return its stdout+err. */
+  async shell(cmd: string): Promise<Result<string>> {
+    const cmdOut = new ResizableArrayBuffer();
+    const streamEndedPromise = defer<string>();
+    const status = await this.createStream(`shell:${cmd}`);
+    if (!status.ok) return status;
+    const stream = status.value;
+    stream.onData = (data: Uint8Array) => cmdOut.append(data);
+    stream.onClose = () => {
+      streamEndedPromise.resolve(utf8Decode(cmdOut.get()));
+    };
+    const outTxt = (await streamEndedPromise).trimEnd();
+    return okResult(outTxt);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts
new file mode 100644
index 0000000..f98d141
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_msg.ts
@@ -0,0 +1,101 @@
+// 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 {assertTrue} from '../../../base/logging';
+import {isString} from '../../../base/object_utils';
+import {binaryEncode, utf8Decode, utf8Encode} from '../../../base/string_utils';
+
+const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
+
+export interface AdbMsgHdr {
+  readonly cmd: string;
+  readonly arg0: number;
+  readonly arg1: number;
+  readonly dataLen: number;
+  readonly dataChecksum: number;
+}
+
+export interface AdbMsg extends AdbMsgHdr {
+  data: Uint8Array;
+}
+
+// A brief description of the message can be found here:
+// https://android.googlesource.com/platform/system/core/+/main/adb/protocol.txt
+//
+// struct amessage {
+//     uint32_t command;    // command identifier constant
+//     uint32_t arg0;       // first argument
+//     uint32_t arg1;       // second argument
+//     uint32_t data_length;// length of payload (0 is allowed)
+//     uint32_t data_check; // checksum of data payload
+//     uint32_t magic;      // command ^ 0xffffffff
+// };
+export function parseAdbMsgHdr(dv: DataView): AdbMsgHdr {
+  assertTrue(dv.byteLength === ADB_MSG_SIZE);
+  const cmd = utf8Decode(dv.buffer.slice(0, 4));
+  const cmdNum = dv.getUint32(0, true);
+  const arg0 = dv.getUint32(4, true);
+  const arg1 = dv.getUint32(8, true);
+  const dataLen = dv.getUint32(12, true);
+  const dataChecksum = dv.getUint32(16, true);
+  const cmdChecksum = dv.getUint32(20, true);
+  const magic = dv.getUint32(20, true);
+  assertTrue(magic === (cmdNum ^ 0xffffffff) >>> 0);
+  assertTrue(cmdNum === (cmdChecksum ^ 0xffffffff));
+  return {cmd, arg0, arg1, dataLen, dataChecksum};
+}
+
+export function encodeAdbMsg(
+  cmd: string,
+  arg0: number,
+  arg1: number,
+  data: Uint8Array,
+  useChecksum = false,
+) {
+  const checksum = useChecksum ? generateChecksum(data) : 0;
+  const buf = new Uint8Array(ADB_MSG_SIZE);
+  const dv = new DataView(buf.buffer);
+  for (let i = 0; i < 4; i++) {
+    dv.setUint8(i, cmd.charCodeAt(i));
+  }
+  dv.setUint32(4, arg0, true);
+  dv.setUint32(8, arg1, true);
+  dv.setUint32(12, data.byteLength, true);
+  dv.setUint32(16, checksum, true);
+  dv.setUint32(20, dv.getUint32(0, true) ^ 0xffffffff, true);
+
+  return buf;
+}
+
+export function encodeAdbData(data?: Uint8Array | string): Uint8Array {
+  if (data === undefined) return new Uint8Array([]);
+  if (isString(data)) return utf8Encode(data + '\0');
+  return data;
+}
+
+function generateChecksum(data: Uint8Array): number {
+  let res = 0;
+  for (let i = 0; i < data.byteLength; i++) res += data[i];
+  return res & 0xffffffff;
+}
+
+export function adbMsgToString(msg: AdbMsg | AdbMsgHdr) {
+  return (
+    `cmd=${msg.cmd}, arg0=${msg.arg0}, arg1=${msg.arg1}, ` +
+    `cksm=${msg.dataChecksum}, dlen=${msg.dataLen}` +
+    ('data' in msg && msg.data !== undefined
+      ? `, data=${binaryEncode(msg.data)}`
+      : '')
+  );
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts
new file mode 100644
index 0000000..8e79367
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_platform_checks.ts
@@ -0,0 +1,77 @@
+// 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 protos from '../../../protos';
+import {errResult, okResult, Result} from '../../../base/result';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {AdbDevice} from './adb_device';
+import {getAdbTracingServiceState} from './adb_tracing_session';
+
+/**
+ * Common pre-flight checks for Android targets. This function is used by
+ * both the AdbWebusbTarget and AdbWebsocketTarget. In both cases we want to
+ * perform the same types of checks regardless of the transport.
+ * @yields a sequence of pre-flight checks.
+ */
+export async function* checkAndroidTarget(
+  adbDevice: AdbDevice,
+): AsyncGenerator<PreflightCheck> {
+  yield {
+    name: 'Android version',
+    status: await (async (): Promise<Result<string>> => {
+      const status = await adbDevice.shell('getprop ro.build.version.sdk');
+      if (!status.ok) return status;
+      const sdkVer = parseInt(status.value);
+      const minApi = 29;
+      if (sdkVer < minApi) {
+        return errResult(`Android API level ${minApi}+ (Q+) required`);
+      }
+      return okResult(`API level ${sdkVer} >= ${minApi}`);
+    })(),
+  };
+  yield {
+    name: 'traced running?',
+    status: await (async (): Promise<Result<string>> => {
+      const status = await adbDevice.shell('pidof traced');
+      if (!status.ok) return status;
+      if (isFinite(parseInt(status.value))) {
+        return okResult(`pid = ${status.value}`);
+      }
+      return errResult(
+        'Not running. Try `adb shell setprop persist.traced.enable 1`',
+      );
+    })(),
+  };
+  const svcStatus = await getAdbTracingServiceState(adbDevice);
+  yield {
+    name: 'Traced version',
+    status: await (async (): Promise<Result<string>> => {
+      if (!svcStatus.ok) return svcStatus;
+      return okResult(svcStatus.value.tracingServiceVersion ?? 'N/A');
+    })(),
+  };
+  if (svcStatus === undefined) return;
+  yield {
+    name: 'Traced state',
+    status: await (async (): Promise<Result<string>> => {
+      if (!svcStatus.ok) return svcStatus;
+      const tss: protos.ITracingServiceState = svcStatus.value;
+      return okResult(
+        `#producers: ${tss.producers?.length ?? 'N/A'}, ` +
+          `#datasources: ${tss.dataSources?.length ?? 'N/A'}, ` +
+          `#sessions: ${tss.numSessionsStarted ?? 'N/A'}`,
+      );
+    })(),
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts
new file mode 100644
index 0000000..9e962d4
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/adb_tracing_session.ts
@@ -0,0 +1,55 @@
+// 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 protos from '../../../protos';
+import {AdbDevice} from './adb_device';
+import {TracingProtocol} from '../tracing_protocol/tracing_protocol';
+import {errResult, okResult, Result} from '../../../base/result';
+import {exists} from '../../../base/utils';
+import {ConsumerIpcTracingSession} from '../tracing_protocol/consumer_ipc_tracing_session';
+
+export const CONSUMER_SOCKET = '/dev/socket/traced_consumer';
+
+export async function createAdbTracingSession(
+  adbDevice: AdbDevice,
+  traceConfig: protos.ITraceConfig,
+): Promise<Result<ConsumerIpcTracingSession>> {
+  const streamStatus = await adbDevice.createStream(
+    `localfilesystem:${CONSUMER_SOCKET}`,
+  );
+  if (!streamStatus.ok) return streamStatus;
+  const stream = streamStatus.value;
+  const consumerIpc = await TracingProtocol.create(stream);
+  const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig);
+  return okResult(session);
+}
+
+export async function getAdbTracingServiceState(
+  adbDevice: AdbDevice,
+): Promise<Result<protos.ITracingServiceState>> {
+  const sock = CONSUMER_SOCKET;
+  const status = await adbDevice.createStream(`localfilesystem:${sock}`);
+  if (!status.ok) {
+    return errResult(`Failed to connect to ${sock}: ${status.error}`);
+  }
+  const stream = status.value;
+  using consumerPort = await TracingProtocol.create(stream);
+  const req = new protos.QueryServiceStateRequest({});
+  const rpcCall = consumerPort.invokeStreaming('QueryServiceState', req);
+  const resp = await rpcCall.promise;
+  if (!exists(resp.serviceState)) {
+    return errResult('Failed to decode QueryServiceStateResponse');
+  }
+  return okResult(resp.serviceState);
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts
new file mode 100644
index 0000000..a468103
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_device.ts
@@ -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.
+
+import {Result, errResult, okResult} from '../../../../base/result';
+import {WebSocketStream} from '../../websocket/websocket_stream';
+import {AdbDevice} from '../adb_device';
+import {adbCmdAndWait} from './adb_websocket_utils';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+
+/**
+ * This class implements the state machine required to communicate with an ADB
+ * device over WebSocket using the perfetto websocket_bridge.
+ * It takes a websocket url as input (which behind the scenes is a plain
+ * bridge to adbd TCP on 127.0.0.1:5037) and a device serial and returns an
+ * object suitable to run shell commands and create streams on it.
+ */
+export class AdbWebsocketDevice extends AdbDevice {
+  private streams = new Array<WebSocketStream>();
+
+  private constructor(
+    private wsUrl: string,
+    private deviceSerial: string,
+    // This socket is only used to tell if we are still connected or not.
+    // Each stream needs a new websocket because of the way the ADB TCP protocol
+    // works.
+    private transportSock: AsyncWebsocket,
+  ) {
+    super();
+  }
+
+  static async connect(
+    wsUrl: string,
+    deviceSerial: string,
+  ): Promise<Result<AdbWebsocketDevice>> {
+    const status = await this.connectToTransport(wsUrl, deviceSerial);
+    if (!status.ok) return status;
+    const sock = status.value;
+    return okResult(new AdbWebsocketDevice(wsUrl, deviceSerial, sock));
+  }
+
+  private static async connectToTransport(
+    wsUrl: string,
+    deviceSerial: string,
+  ): Promise<Result<AsyncWebsocket>> {
+    const sock = await AsyncWebsocket.connect(wsUrl);
+    if (sock === undefined) {
+      return errResult(`Connection to ${wsUrl} failed`);
+    }
+    const transport = `host:transport:${deviceSerial}`;
+    const status = await adbCmdAndWait(sock, transport, false);
+    if (!status.ok) return status;
+    return okResult(sock);
+  }
+
+  override async createStream(svc: string): Promise<Result<WebSocketStream>> {
+    const connRes = await AdbWebsocketDevice.connectToTransport(
+      this.wsUrl,
+      this.deviceSerial,
+    );
+    if (!connRes.ok) return connRes;
+    const sock = connRes.value;
+    const status = await adbCmdAndWait(sock, svc, false);
+    if (!status.ok) return status;
+    const stream = new WebSocketStream(sock.release());
+    this.streams.push(stream);
+    return okResult(stream);
+  }
+
+  get connected(): boolean {
+    return this.transportSock.connected;
+  }
+
+  override close(): void {
+    this.transportSock.close();
+    this.streams.forEach((s) => s.close());
+    this.streams.splice(0);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts
new file mode 100644
index 0000000..5bc6dd7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target.ts
@@ -0,0 +1,95 @@
+// 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 protos from '../../../../protos';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {RecordingTarget} from '../../interfaces/recording_target';
+import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
+import {checkAndroidTarget} from '../adb_platform_checks';
+import {
+  createAdbTracingSession,
+  getAdbTracingServiceState,
+} from '../adb_tracing_session';
+import {AdbWebsocketDevice} from './adb_websocket_device';
+import {AsyncLazy} from '../../../../base/async_lazy';
+
+export class AdbWebsocketTarget implements RecordingTarget {
+  readonly kind = 'LIVE_RECORDING';
+  readonly platform = 'ANDROID';
+  readonly transportType = 'WebSocket';
+
+  private adbDevice = new AsyncLazy<AdbWebsocketDevice>();
+
+  constructor(
+    private wsUrl: string,
+    private serial: string,
+    private model: string,
+  ) {}
+
+  get id(): string {
+    return this.serial;
+  }
+
+  get name(): string {
+    return `${this.model} [${this.serial}]`;
+  }
+
+  get connected(): boolean {
+    return this.adbDevice.value?.connected ?? false;
+  }
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    yield {
+      name: 'WebSocket connection',
+      status: await (async (): Promise<Result<string>> => {
+        const status = await this.connectIfNeeded();
+        if (!status.ok) return status;
+        return okResult('connected');
+      })(),
+    };
+    if (this.adbDevice.value === undefined) return;
+    yield* checkAndroidTarget(this.adbDevice.value);
+  }
+
+  private async connectIfNeeded(): Promise<Result<AdbWebsocketDevice>> {
+    return this.adbDevice.getOrCreate(() =>
+      AdbWebsocketDevice.connect(this.wsUrl, this.serial),
+    );
+  }
+
+  disconnect(): void {
+    // There isn't much to do in this case. If the device is disconnected,
+    // the per-stream sockets will be naturally closed by adb. In turn,
+    // websocket_bridge will propagate that as a closure of the per-stream
+    // WebSockets.
+    this.adbDevice.value?.close();
+    this.adbDevice.reset();
+  }
+
+  async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+    if (this.adbDevice.value === undefined) {
+      return errResult('WebSocket transport disconnected');
+    }
+    return getAdbTracingServiceState(this.adbDevice.value);
+  }
+
+  async startTracing(
+    traceConfig: protos.ITraceConfig,
+  ): Promise<Result<ConsumerIpcTracingSession>> {
+    const adbDeviceStatus = await this.connectIfNeeded();
+    if (!adbDeviceStatus.ok) return adbDeviceStatus;
+    return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts
new file mode 100644
index 0000000..54592bf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_target_provider.ts
@@ -0,0 +1,98 @@
+// 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 {errResult, okResult, Result} from '../../../../base/result';
+import {exists} from '../../../../base/utils';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+import {RecordingTargetProvider} from '../../interfaces/recording_target_provider';
+import {AdbWebsocketTarget} from './adb_websocket_target';
+import {adbCmdAndWait} from './adb_websocket_utils';
+import {EvtSource} from '../../../../base/events';
+import {websocketInstructions} from '../../websocket/websocket_utils';
+
+export class AdbWebsocketTargetProvider implements RecordingTargetProvider {
+  readonly id = 'adb_websocket';
+  readonly name = 'ADB + WebSocket';
+  readonly description =
+    'This option uses the adbd server and can co-exist with other ' +
+    'adb-based tools. Requires launching the websocket_bridge on the host.';
+  readonly icon = 'lan';
+  readonly supportedPlatforms = ['ANDROID'] as const;
+  private readonly wsHost = '127.0.0.1:8037';
+  readonly onTargetsChanged = new EvtSource<void>();
+  private targets = new Map<string, AdbWebsocketTarget>();
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    yield {
+      name: 'WebSocket connection',
+      status: await (async (): Promise<Result<string>> => {
+        using sock = await AsyncWebsocket.connect(this.wsUrl);
+        return sock
+          ? okResult('Connected')
+          : errResult(
+              `Failed to connect ${this.wsUrl}. ` +
+                websocketInstructions('ANDROID'),
+            );
+      })(),
+    };
+  }
+
+  async listTargets(): Promise<AdbWebsocketTarget[]> {
+    await this.refreshTargets();
+    return Array.from(this.targets.values());
+  }
+
+  private async refreshTargets() {
+    const adbDevices = await this.listAdbdDevices();
+    // Find and disconnected devices.
+    for (const [serial, target] of this.targets.entries()) {
+      if (!adbDevices.has(serial)) {
+        target.disconnect();
+        this.targets.delete(serial);
+      }
+    }
+    // Find new devices.
+    for (const [serial, model] of adbDevices.entries()) {
+      if (this.targets.has(serial)) continue; // We already have a target.
+      const newTarget = new AdbWebsocketTarget(this.wsUrl, serial, model);
+      this.targets.set(serial, newTarget);
+    }
+  }
+
+  // Returns a map of device serial -> product.
+  private async listAdbdDevices(): Promise<Map<string, string>> {
+    const devices = new Map<string, string>();
+    using sock = await AsyncWebsocket.connect(this.wsUrl);
+    if (!sock) return devices;
+    const status = await adbCmdAndWait(sock, 'host:devices-l', true);
+    if (!status.ok) return devices;
+    for (const line of status.value.trimEnd().split('\n')) {
+      if (line === '') continue;
+      const m = line.match(/^(\w+)\s+.*model:([^ ]+)/);
+      if (!exists(m)) {
+        console.warn('Could not parse ADB device', line);
+        continue;
+      }
+      const serial = m[1];
+      const model = m[2];
+      devices.set(serial, model);
+    }
+    return devices;
+  }
+
+  private get wsUrl(): string {
+    return `ws://${this.wsHost}/adb`;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts
new file mode 100644
index 0000000..486802d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/websocket/adb_websocket_utils.ts
@@ -0,0 +1,49 @@
+// 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 {assertTrue} from '../../../../base/logging';
+import {Result, okResult, errResult} from '../../../../base/result';
+import {AsyncWebsocket} from '../../websocket/async_websocket';
+import {prefixWithHexLen} from '../../websocket/websocket_utils';
+
+/**
+ * Sends an ADB command over the websocket and waits for an OKAY or FAIL.
+ * If `wantResponse` == true, expects a payload after the OKAY.
+ * For all intents and purposes, the websocket here is the moral equivalent of
+ * talking directly to ADB on 127.0.0.1:5037.
+ * See //packages/modules/adb/docs/dev/services.md .
+ */
+export async function adbCmdAndWait(
+  ws: AsyncWebsocket,
+  cmd: string,
+  wantResponse: boolean,
+): Promise<Result<string>> {
+  ws.send(prefixWithHexLen(cmd));
+  const hdr = await ws.waitForString(4);
+  if (hdr === 'FAIL' || (hdr === 'OKAY' && wantResponse)) {
+    const hexLen = await ws.waitForString(4);
+    const len = parseInt(hexLen, 16);
+    assertTrue(!isNaN(len));
+    const payload = await ws.waitForString(len);
+    if (hdr === 'OKAY') {
+      return okResult(payload);
+    } else {
+      return errResult(payload);
+    }
+  } else if (hdr === 'OKAY') {
+    return okResult('');
+  } else {
+    return errResult(`ADB protocol error, hdr ${hdr}`);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
similarity index 95%
rename from ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
rename to ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
index 7ed275e..6f4d139 100644
--- a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key.ts
@@ -19,7 +19,6 @@
   base64Encode,
   hexEncode,
 } from '../../../../base/string_utils';
-import {RecordingError} from '../recording_error_handling';
 
 const WORD_SIZE = 4;
 const MODULUS_SIZE_BITS = 2048;
@@ -54,36 +53,6 @@
   qi: string;
 }
 
-function isValidJsonWebKey(key: JsonWebKey): key is ValidJsonWebKey {
-  return (
-    key.n !== undefined &&
-    key.e !== undefined &&
-    key.d !== undefined &&
-    key.p !== undefined &&
-    key.q !== undefined &&
-    key.dp !== undefined &&
-    key.dq !== undefined &&
-    key.qi !== undefined
-  );
-}
-
-// Convert a BigInteger to an array of a specified size in bytes.
-function bigIntToFixedByteArray(bn: BigInteger, size: number): Uint8Array {
-  const paddedBnBytes = bn.toByteArray();
-  let firstNonZeroIndex = 0;
-  while (
-    firstNonZeroIndex < paddedBnBytes.length &&
-    paddedBnBytes[firstNonZeroIndex] === 0
-  ) {
-    firstNonZeroIndex++;
-  }
-  const bnBytes = Uint8Array.from(paddedBnBytes.slice(firstNonZeroIndex));
-  const res = new Uint8Array(size);
-  assertTrue(bnBytes.length <= res.length);
-  res.set(bnBytes, res.length - bnBytes.length);
-  return res;
-}
-
 export class AdbKey {
   // We use this JsonWebKey to:
   // - create a private key and sign with it
@@ -92,11 +61,15 @@
   // from the device and deserialize)
   jwkPrivate: ValidJsonWebKey;
 
+  static deserialize(serializedKey: string): AdbKey {
+    return new AdbKey(JSON.parse(serializedKey));
+  }
+
   private constructor(jwkPrivate: ValidJsonWebKey) {
     this.jwkPrivate = jwkPrivate;
   }
 
-  static async GenerateNewKeyPair(): Promise<AdbKey> {
+  static async generateNewKeyPair(): Promise<AdbKey> {
     // Construct a new CryptoKeyPair and keep its private key in JWB format.
     const keyPair = await crypto.subtle.generateKey(
       ADB_WEB_CRYPTO_ALGORITHM,
@@ -105,15 +78,11 @@
     );
     const jwkPrivate = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
     if (!isValidJsonWebKey(jwkPrivate)) {
-      throw new RecordingError('Could not generate a valid private key.');
+      throw new Error('Could not generate a valid ADB private key');
     }
     return new AdbKey(jwkPrivate);
   }
 
-  static DeserializeKey(serializedKey: string): AdbKey {
-    return new AdbKey(JSON.parse(serializedKey));
-  }
-
   // Perform an RSA signing operation for the ADB auth challenge.
   //
   // For the RSA signature, the token is expected to have already
@@ -193,7 +162,37 @@
     return base64Encode(dvU8) + ' ui.perfetto.dev';
   }
 
-  serializeKey(): string {
+  serialize(): string {
     return JSON.stringify(this.jwkPrivate);
   }
 }
+
+function isValidJsonWebKey(key: JsonWebKey): key is ValidJsonWebKey {
+  return (
+    key.n !== undefined &&
+    key.e !== undefined &&
+    key.d !== undefined &&
+    key.p !== undefined &&
+    key.q !== undefined &&
+    key.dp !== undefined &&
+    key.dq !== undefined &&
+    key.qi !== undefined
+  );
+}
+
+// Convert a BigInteger to an array of a specified size in bytes.
+function bigIntToFixedByteArray(bn: BigInteger, size: number): Uint8Array {
+  const paddedBnBytes = bn.toByteArray();
+  let firstNonZeroIndex = 0;
+  while (
+    firstNonZeroIndex < paddedBnBytes.length &&
+    paddedBnBytes[firstNonZeroIndex] === 0
+  ) {
+    firstNonZeroIndex++;
+  }
+  const bnBytes = Uint8Array.from(paddedBnBytes.slice(firstNonZeroIndex));
+  const res = new Uint8Array(size);
+  assertTrue(bnBytes.length <= res.length);
+  res.set(bnBytes, res.length - bnBytes.length);
+  return res;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts
new file mode 100644
index 0000000..957e1c6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_key_manager.ts
@@ -0,0 +1,108 @@
+// Copyright (C) 2022 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 {assetSrc} from '../../../../base/assets';
+import {AsyncLazy} from '../../../../base/async_lazy';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {exists} from '../../../../base/utils';
+import {AdbKey} from './adb_key';
+
+// How long we will store the key in memory
+const KEY_IN_MEMORY_TIMEOUT = 1000 * 60 * 30; // 30 minutes
+
+export class AdbKeyManager {
+  // private key?: AdbKey;
+  private expiryTimerId = -1;
+  private key = new AsyncLazy<AdbKey>();
+  // private asyncGuard = new AsyncGuard<AdbKey | undefined>();
+
+  // Finds a key, by priority:
+  // - Look in memory (i.e. this.key)
+  // - Look in the credential store.
+  // - Finally creates one from scratch if needed.
+  async getOrCreateKey(): Promise<Result<AdbKey>> {
+    this.refreshKeyExpiry();
+    return this.key.getOrCreate(async () => {
+      // 2. We try to get the private key from the browser.
+      // The mediation is set as 'optional', because we use
+      // 'preventSilentAccess', which sometimes requests the user to click
+      // on a button to allow the auth, but sometimes only shows a
+      // notification and does not require the user to click on anything.
+      // If we had set mediation to 'required', the user would have been
+      // asked to click on a button every time.
+      if (hasPasswordCredential()) {
+        const options: PasswordCredentialRequestOptions = {
+          password: true,
+          mediation: 'optional',
+        };
+        const credential = await navigator.credentials.get(options);
+        await navigator.credentials.preventSilentAccess();
+        if (exists(credential) && 'password' in credential) {
+          return okResult(AdbKey.deserialize(credential.password as string));
+        }
+      }
+
+      // This can happen in two cases:
+      // 1. The very first time when we have no credentials saved.
+      // 2. If the user (accidentally) dismisses the "sign in" dialog.
+      // We use this UX to prevent that if the user accidentally clicks Escape,
+      // we invalidate the key and generates a new one, which would be
+      // unauthorized.
+      if (!confirm("Couldn't load the ADB key. Generate a new key?")) {
+        return errResult(
+          "Couldn't load the ADB Key. " + 'Did you dismiss the sign-in dialog',
+        );
+      }
+
+      // 3. We generate a new key pair.
+      const newKey = await AdbKey.generateNewKeyPair();
+      await storeKeyInBrowserCredentials(newKey);
+      return okResult(newKey);
+    });
+  }
+
+  private refreshKeyExpiry() {
+    if (this.expiryTimerId >= 0) {
+      clearTimeout(this.expiryTimerId);
+    }
+    this.expiryTimerId = self.setTimeout(
+      () => this.key.reset(),
+      KEY_IN_MEMORY_TIMEOUT,
+    );
+  }
+}
+
+// Update credential store with the given key.
+async function storeKeyInBrowserCredentials(key: AdbKey): Promise<void> {
+  if (!hasPasswordCredential()) {
+    return;
+  }
+  const credential = new PasswordCredential({
+    id: 'webusb-adb-key',
+    password: key.serialize(),
+    name: 'WebUSB ADB Key',
+    iconURL: assetSrc('assets/favicon.png'),
+  });
+  // The 'Save password?' Chrome dialogue only appears if the key is
+  // not already stored in Chrome.
+  await navigator.credentials.store(credential);
+  // 'preventSilentAccess' guarantees the user is always notified when
+  // credentials are accessed. Sometimes the user is asked to click a button
+  // and other times only a notification is shown temporarily.
+  await navigator.credentials.preventSilentAccess();
+}
+
+function hasPasswordCredential() {
+  return 'PasswordCredential' in window;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts
new file mode 100644
index 0000000..22f19a9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_device.ts
@@ -0,0 +1,435 @@
+// 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 {defer, Deferred} from '../../../../base/deferred';
+import {assertFalse, assertTrue} from '../../../../base/logging';
+import {isString} from '../../../../base/object_utils';
+import {hexEncode, utf8Decode, utf8Encode} from '../../../../base/string_utils';
+import {exists} from '../../../../base/utils';
+import {closeModal, showModal} from '../../../../widgets/modal';
+import {AdbKeyManager} from './adb_key_manager';
+import {AdbDevice} from '../adb_device';
+import {
+  encodeAdbMsg,
+  encodeAdbData,
+  parseAdbMsgHdr,
+  AdbMsg,
+  adbMsgToString,
+} from '../adb_msg';
+import {getAdbWebUsbInterface, AdbUsbInterface} from './adb_webusb_utils';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {AdbWebusbStream} from './adb_webusb_stream';
+
+const ADB_MSG_SIZE = 6 * 4; // 6 * int32.
+const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
+const VERSION_WITH_CHECKSUM = 0x01000000;
+const VERSION_NO_CHECKSUM = 0x01000001;
+
+/**
+ * This class implements the state machine required to communicate with an ADB
+ * device over WebUsb. It takes a {@link USBDevice} in input and returns an
+ * object suitable to run shell commands and create streams on it.
+ */
+export class AdbWebusbDevice extends AdbDevice {
+  private lastStreamId = 0;
+  private _connected = true;
+  private rxLoopRunning = false;
+  private streams = new Map<number, AdbWebusbStream>();
+  private pendingStreams = new Map<number, PendingStream>();
+  private txQueue = new Array<TxQueueEntry>();
+  private txPending = false;
+
+  /** Use {@link connect()} to obtain an instance of this class. */
+  private constructor(
+    private readonly usb: AdbUsbInterface,
+    private readonly maxPayload: number,
+    private readonly useChecksum: boolean,
+  ) {
+    super();
+    this.usb = usb;
+    // Deliberately not awaited, the rx looop will loop forever in the
+    // background until we disconnect.
+    this.usbRxLoop();
+  }
+
+  /**
+   * Creates a new instance of this class.
+   * @param usbdev the device obtained via {@link navigator.usb.requestDevice}.
+   * @param adbKeyMgr an instance of the key manager.
+   */
+  static async connect(
+    usbdev: USBDevice,
+    adbKeyMgr: AdbKeyManager,
+  ): Promise<Result<AdbWebusbDevice>> {
+    const usb = getAdbWebUsbInterface(usbdev);
+    if (usb === undefined) {
+      return errResult(
+        'Could not find the USB Interface. ' +
+          'Try disconnecting and reconnecting the device.',
+      );
+    }
+    if (usbdev.opened) {
+      await usbdev.close();
+    }
+    await usbdev.open();
+    using autoClose = new CloseDeviceWhenOutOfScope(usbdev);
+    await usbdev.selectConfiguration(usb.configurationValue);
+
+    try {
+      await usbdev.claimInterface(usb.usbInterfaceNumber);
+    } catch (err) {
+      console.error(err);
+      return errResult(
+        'Failed to claim USB interface. Try `adb kill-server` or ' +
+          'close other profiling tools and try again',
+      );
+    }
+
+    const keyRes = await adbKeyMgr.getOrCreateKey();
+    if (!keyRes.ok) return keyRes;
+    const key = keyRes.value;
+
+    await AdbWebusbDevice.send(
+      usb,
+      'CNXN',
+      VERSION_NO_CHECKSUM,
+      DEFAULT_MAX_PAYLOAD_BYTES,
+      'host:1:WebUsb',
+    );
+
+    // At this point there are two options:
+    // 1. The device accepts the key and responds with a CNXN msg.
+    // 2. The device doesn't recognize us, and responds with another AUTH msg.
+
+    // We need to have some tolerance from queued messages from previous
+    // sessions, hence the 10 attempts to deal with spurious messages.
+    let authAttempts = 0;
+    const modalKey = 'adbauth';
+    for (let attempt = 0; attempt < 10; attempt++) {
+      const msg = await this.recvMsg(usb);
+
+      if (msg.cmd === 'CNXN') {
+        // Success, the device authenticated us.
+        closeModal(modalKey);
+        const maxPayload = msg.arg1;
+        const ver = msg.arg0;
+        if (ver !== VERSION_WITH_CHECKSUM && ver !== VERSION_NO_CHECKSUM) {
+          return errResult(`ADB version ${ver} not supported`);
+        }
+        const useChecksum = ver === VERSION_WITH_CHECKSUM;
+        autoClose.keepOpen = true;
+        return okResult(new AdbWebusbDevice(usb, maxPayload, useChecksum));
+      }
+
+      if (msg.cmd !== 'AUTH') {
+        logSpuriousMsg(msg);
+        continue;
+      }
+
+      assertTrue(msg.arg0 === AuthCmd.TOKEN);
+      const authAttempt = authAttempts++;
+      if (authAttempt === 0) {
+        // Case 1: we are presented with a nonce to sign. If the device has
+        // previously received our public key, the dialog asking for user
+        // confirmation will NOT be displayed.
+        const signedNonce = key.sign(msg.data);
+        await this.send(usb, 'AUTH', AuthCmd.SIGNATURE, 0, signedNonce);
+        continue;
+      }
+      if (authAttempt === 1) {
+        // Case 2: present our public key. This will prompt the dialog.
+        await this.send(usb, 'AUTH', AuthCmd.PUBKEY, 0, key.getPublicKey());
+        showModal({
+          key: modalKey,
+          title: 'ADB Authorization required',
+          content: 'Please unlock the device and authorize the ADB connection',
+        });
+        continue;
+      }
+      break;
+    }
+    return errResult('ADB authorization failed');
+  }
+
+  override async createStream(svc: string): Promise<Result<AdbWebusbStream>> {
+    const ps: PendingStream = {
+      promise: defer<Result<AdbWebusbStream>>(),
+      localId: ++this.lastStreamId,
+      svc,
+    };
+    this.pendingStreams.set(ps.localId, ps);
+    this.send('OPEN', ps.localId, 0, svc);
+    return ps.promise;
+  }
+
+  override close(): void {
+    this._connected = false;
+    this.usb.dev.opened && this.usb.dev.close();
+    this.streams.forEach((stream) => this.streamClose(stream));
+  }
+
+  get connected() {
+    return this._connected;
+  }
+
+  streamWrite(
+    stream: AdbWebusbStream,
+    data: string | Uint8Array,
+  ): Promise<void> {
+    const promise = defer<void>();
+    const raw = isString(data) ? utf8Encode(data) : data;
+    let sent = 0;
+    while (sent < raw.byteLength) {
+      const chunkLen = Math.min(this.maxPayload, raw.byteLength - sent);
+      const chunk = raw.subarray(sent, sent + chunkLen);
+      sent += chunkLen;
+      const tx: TxQueueEntry = {
+        stream,
+        data: chunk,
+        // This is the last chunk. Attach the promise only to the last chunk.
+        promise: sent === raw.byteLength ? promise : undefined,
+      };
+      this.txQueue.push(tx);
+      if (!this.txPending) {
+        assertTrue(this.txQueue.length === 1);
+        this.streamWriteFromQueue(tx);
+      }
+    }
+    return promise;
+  }
+
+  streamClose(stream: AdbWebusbStream): void {
+    // Remove any pending entry from the tx queue.
+    this.txQueue = this.txQueue.filter((tx) => tx.stream !== stream);
+    this.send('CLSE', stream.localId, stream.remoteId);
+    this.streams.delete(stream.localId);
+    stream.notifyClose();
+  }
+
+  private streamWriteFromQueue(tx: TxQueueEntry) {
+    assertFalse(this.txPending);
+    this.txPending = true;
+    this.send('WRTE', tx.stream.localId, tx.stream.remoteId, tx.data);
+  }
+
+  private async usbRxLoop(): Promise<void> {
+    assertFalse(this.rxLoopRunning);
+    this.rxLoopRunning = true;
+    try {
+      while (this._connected) {
+        await this.usbRxLoopInner();
+      }
+    } catch (e) {
+      // We allow the transferIn() in recv() to fail if we disconnected. That
+      // will naturally happen in the [Symbol.dispose].
+      const transferInAborted =
+        e instanceof Error && e.message.includes('transfer was cancelled');
+      if (!(transferInAborted && !this._connected)) {
+        throw e;
+      }
+    } finally {
+      this.rxLoopRunning = false;
+      this._connected = false;
+    }
+  }
+
+  private async usbRxLoopInner(): Promise<void> {
+    const msg = await AdbWebusbDevice.recvMsg(this.usb);
+
+    if (msg.cmd === 'OKAY') {
+      // There are two cases here:
+      // 1) This is an ACK to an OPEN (new stream).
+      // 2) This is an ACK to a WRTE on an existing stream.
+      const remoteStreamId = msg.arg0;
+      const localStreamId = msg.arg1;
+      const pendingStream = this.pendingStreams.get(localStreamId);
+      if (pendingStream !== undefined) {
+        // Case 1.
+        this.pendingStreams.delete(localStreamId);
+        const stream = new AdbWebusbStream(this, localStreamId, remoteStreamId);
+        this.streams.set(localStreamId, stream);
+        pendingStream.promise.resolve(okResult(stream));
+      } else {
+        // Case 2.
+        const queuedEntry = this.popFromTxQueue(localStreamId, remoteStreamId);
+        if (queuedEntry === undefined) {
+          return logSpuriousMsg(msg);
+        }
+        this.txPending = false;
+        queuedEntry.promise?.resolve();
+        const next = this.txQueue[0];
+        next !== undefined && this.streamWriteFromQueue(next);
+      }
+      return;
+    } else if (msg.cmd === 'WRTE') {
+      const localStreamId = msg.arg1;
+      const stream = this.streams.get(localStreamId);
+      if (stream === undefined) {
+        return logSpuriousMsg(msg);
+      }
+      await this.send('OKAY', stream.localId, stream.remoteId);
+      stream.onData(msg.data);
+    } else if (msg.cmd === 'CLSE') {
+      // Close a stream.
+      const localStreamId = msg.arg1;
+
+      // If the stream has not been opened yet, this is a failure while opening.
+      const ps = this.pendingStreams.get(localStreamId);
+      if (ps !== undefined) {
+        this.pendingStreams.delete(localStreamId);
+        ps.promise.resolve(errResult(`Stream ${ps.svc} failed to connect`));
+        return;
+      }
+
+      // Otherwise the service is telling us about a stream getting closed from
+      // their end (e.g. the shell:xxx command terminated).
+      const stream = this.streams.get(localStreamId);
+      // If we initiate the closure, the stream entry is already removed.
+      if (stream !== undefined) {
+        this.streams.delete(localStreamId);
+        stream.notifyClose();
+      }
+    } else {
+      console.error(`Unexpected ADB cmd ${msg.cmd} ${msg.arg0} ${msg.arg1}`);
+    }
+  }
+
+  private popFromTxQueue(
+    localStreamId: number,
+    remoteStreamId: number,
+  ): TxQueueEntry {
+    for (let i = 0; i < this.txQueue.length; i++) {
+      const tx = this.txQueue[i];
+      if (tx.stream.localId !== localStreamId) continue;
+      if (tx.stream.remoteId !== remoteStreamId) continue;
+      return this.txQueue.splice(i, 1)[0];
+    }
+    throw new WebusbTransportError(
+      `Could not find ADB queue entry L=${localStreamId}, ` +
+        `R=${remoteStreamId}, TxLen=${this.txQueue.length}`,
+    );
+  }
+
+  private static async recv(
+    usb: AdbUsbInterface,
+    len: number,
+  ): Promise<DataView> {
+    const res = await usb.dev.transferIn(usb.rx, len);
+    if (!exists(res.data) || res.status !== 'ok') {
+      throw new WebusbTransportError(`res: ${res.status}, data: ${!!res.data}`);
+    }
+    return res.data;
+  }
+
+  private static async recvMsg(usb: AdbUsbInterface): Promise<AdbMsg> {
+    const hdrData = await this.recv(usb, ADB_MSG_SIZE);
+    if (hdrData.byteLength !== ADB_MSG_SIZE) {
+      const arr = new Uint8Array(hdrData.buffer);
+      throw new WebusbTransportError(
+        `RX spurious: ${hexEncode(arr)} ${utf8Decode(arr)}`,
+      );
+    }
+    const hdr = parseAdbMsgHdr(hdrData);
+    let payload = new Uint8Array();
+    if (hdr.dataLen > 0) {
+      const payloadData = await this.recv(usb, hdr.dataLen);
+      payload = new Uint8Array(
+        payloadData.buffer,
+        payloadData.byteOffset,
+        payloadData.byteLength,
+      ).slice();
+    }
+    return {...hdr, data: payload};
+  }
+
+  private send(
+    cmd: string,
+    arg0: number,
+    arg1: number,
+    data?: Uint8Array | string,
+  ): Promise<void> {
+    if (!this.connected) return Promise.resolve();
+    const useCksum = this.useChecksum;
+    return AdbWebusbDevice.send(this.usb, cmd, arg0, arg1, data, useCksum);
+  }
+
+  private static async send(
+    usb: AdbUsbInterface,
+    cmd: string,
+    arg0: number,
+    arg1: number,
+    data?: Uint8Array | string,
+    useChecksum = false,
+  ): Promise<void> {
+    const payload = encodeAdbData(data);
+    const header = encodeAdbMsg(cmd, arg0, arg1, payload, useChecksum);
+
+    // The header and the message data must be sent consecutively. In order to
+    // avoid interleaving ([hdr1] [hdr2] [data1] [data2]), we chain promises.
+    const sendPromises = [usb.dev.transferOut(usb.tx, header.buffer)];
+    if (payload.length > 0) {
+      sendPromises.push(usb.dev.transferOut(usb.tx, payload.buffer));
+      if (payload.length % usb.txPacketSize === 0) {
+        // if the number of bytes transferred fits exactly into packets then
+        // we need an extra zero length packet at the end.
+        sendPromises.push(usb.dev.transferOut(usb.tx, new Uint8Array(0)));
+      }
+    }
+    await Promise.all(sendPromises);
+  }
+}
+
+enum AuthCmd {
+  TOKEN = 1,
+  SIGNATURE = 2,
+  PUBKEY = 3,
+}
+
+interface TxQueueEntry {
+  stream: AdbWebusbStream;
+  data: Uint8Array;
+  promise?: Deferred<void>;
+}
+
+interface PendingStream {
+  promise: Deferred<Result<AdbWebusbStream>>;
+  localId: number;
+  svc: string; // The service being requested, e.g. 'shell:whoami'.
+}
+
+class WebusbTransportError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = 'WebusbTransportError';
+  }
+}
+
+// These log messages are non-fatal because we need to tolerate the fact that
+// adbd can buffer messages from previous connections (e.g. if reloading a tab)
+// and won't clear the queue when we restart the flow (as one would expect).
+function logSpuriousMsg(msg: AdbMsg): void {
+  console.log('Spurious ADB message', adbMsgToString(msg));
+}
+
+class CloseDeviceWhenOutOfScope {
+  constructor(private usbdev: USBDevice) {}
+  keepOpen = false;
+
+  [Symbol.dispose]() {
+    if (this.keepOpen) return;
+    if (this.usbdev.opened) {
+      this.usbdev.close();
+    }
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts
new file mode 100644
index 0000000..31357d5
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_stream.ts
@@ -0,0 +1,58 @@
+// 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 {ByteStream} from '../../interfaces/byte_stream';
+import {AdbWebusbDevice} from './adb_webusb_device';
+
+export class AdbWebusbStream extends ByteStream {
+  private state: 'CONNECTED' | 'CLOSING' | 'CLOSED' = 'CONNECTED';
+
+  constructor(
+    private adbWebusbDevice: AdbWebusbDevice,
+    readonly localId: number,
+    readonly remoteId: number,
+  ) {
+    super();
+  }
+
+  get connected(): boolean {
+    return this.state === 'CONNECTED';
+  }
+
+  write(data: string | Uint8Array): Promise<void> {
+    if (this.state !== 'CONNECTED') {
+      // Ignore writes queued once the stream is being closed.
+      return Promise.resolve();
+    }
+    return this.adbWebusbDevice.streamWrite(this, data);
+  }
+
+  // This is invoked by the user to request closure. This is the case when the
+  // closure is initiated by the caller (e.g. terminating a shell process).
+  close(): void {
+    if (this.state !== 'CONNECTED') return;
+    this.state = 'CLOSING';
+    this.adbWebusbDevice.streamClose(this);
+  }
+
+  // Called by AdbWebusbTransport in two cases:
+  // 1. To ACK a closure request, if we are in state = 'CLOSING'.
+  // 2. To inform us about device-side closure (e.g. the process terminated)
+  //    if we are in state 'CONNECTED'.
+  notifyClose() {
+    if (this.state === 'CLOSED') return;
+    this.state = 'CLOSED';
+    this.onClose();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts
new file mode 100644
index 0000000..e9c5322
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target.ts
@@ -0,0 +1,94 @@
+// 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 protos from '../../../../protos';
+import {RecordingTarget} from '../../interfaces/recording_target';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AdbKeyManager} from './adb_key_manager';
+import {
+  createAdbTracingSession,
+  getAdbTracingServiceState,
+} from '../adb_tracing_session';
+import {AdbWebusbDevice} from './adb_webusb_device';
+import {AdbUsbInterface, usbDeviceToStr} from './adb_webusb_utils';
+import {errResult, okResult, Result} from '../../../../base/result';
+import {checkAndroidTarget} from '../adb_platform_checks';
+import {ConsumerIpcTracingSession} from '../../tracing_protocol/consumer_ipc_tracing_session';
+import {AsyncLazy} from '../../../../base/async_lazy';
+
+export class AdbWebusbTarget implements RecordingTarget {
+  readonly kind = 'LIVE_RECORDING';
+  readonly platform = 'ANDROID';
+  readonly transportType = 'WebUSB';
+  private adbDevice = new AsyncLazy<AdbWebusbDevice>();
+
+  constructor(
+    private usbiface: AdbUsbInterface,
+    private adbKeyMgr: AdbKeyManager,
+  ) {}
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    const status = await this.connectIfNeeded();
+
+    yield {
+      name: 'WebUSB connection',
+      status: await (async (): Promise<Result<string>> => {
+        if (!status.ok) return status;
+        return okResult('connected');
+      })(),
+    };
+
+    if (this.adbDevice.value === undefined) return;
+    yield* checkAndroidTarget(this.adbDevice.value);
+  }
+
+  async connectIfNeeded(): Promise<Result<AdbWebusbDevice>> {
+    return this.adbDevice.getOrCreate(() =>
+      AdbWebusbDevice.connect(this.usbiface.dev, this.adbKeyMgr),
+    );
+  }
+
+  get connected(): boolean {
+    return this.adbDevice.value?.connected ?? false;
+  }
+
+  get id(): string {
+    return usbDeviceToStr(this.usbiface.dev);
+  }
+
+  get name(): string {
+    const dev = this.usbiface.dev;
+    return `${dev.productName} [${dev.serialNumber}]`;
+  }
+
+  async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+    if (this.adbDevice.value === undefined) {
+      return errResult('WebUSB transport disconnected');
+    }
+    return getAdbTracingServiceState(this.adbDevice.value);
+  }
+
+  async startTracing(
+    traceConfig: protos.ITraceConfig,
+  ): Promise<Result<ConsumerIpcTracingSession>> {
+    const adbDeviceStatus = await this.connectIfNeeded();
+    if (!adbDeviceStatus.ok) return adbDeviceStatus;
+    return await createAdbTracingSession(adbDeviceStatus.value, traceConfig);
+  }
+
+  disconnect(): void {
+    this.adbDevice.value?.close();
+    this.adbDevice.reset();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts
new file mode 100644
index 0000000..c962a7b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_target_provider.ts
@@ -0,0 +1,132 @@
+// 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 {exists} from '../../../../base/utils';
+import {PreflightCheck} from '../../interfaces/connection_check';
+import {AdbKeyManager} from './adb_key_manager';
+import {
+  ADB_DEVICE_FILTER,
+  AdbUsbInterface,
+  getAdbWebUsbInterface,
+  usbDeviceToStr,
+} from './adb_webusb_utils';
+import {errResult} from '../../../../base/result';
+import {RecordingTargetProvider} from '../../interfaces/recording_target_provider';
+import {AdbWebusbTarget} from './adb_webusb_target';
+import {EvtSource} from '../../../../base/events';
+
+export class AdbWebusbTargetProvider implements RecordingTargetProvider {
+  readonly id = 'adb_webusb';
+  readonly name = 'WebUsb';
+  readonly icon = 'usb';
+  readonly supportedPlatforms = ['ANDROID'] as const;
+  readonly description =
+    'This is the easiest option to use but requires exclusive access to the ' +
+    'device. If you are an android developer and use ADB, you should use the ' +
+    'websocket option instead.';
+
+  private adbKeyMgr = new AdbKeyManager();
+  private targets = new Map<string, AdbWebusbTarget>();
+  readonly onTargetsChanged = new EvtSource<void>();
+
+  constructor() {
+    if (!exists(navigator.usb)) return;
+    navigator.usb.addEventListener('disconnect', () => this.refreshTargets());
+    navigator.usb.addEventListener('connect', () => this.refreshTargets());
+  }
+
+  async listTargets(): Promise<AdbWebusbTarget[]> {
+    if (!exists(navigator.usb)) return [];
+    await this.refreshTargets();
+    return Array.from(this.targets.values());
+  }
+
+  async pairNewTarget(): Promise<AdbWebusbTarget | undefined> {
+    if (!exists(navigator.usb)) return undefined;
+    let usbdev: USBDevice;
+    try {
+      usbdev = await navigator.usb.requestDevice({
+        filters: [ADB_DEVICE_FILTER],
+      });
+    } catch (err) {
+      if (`${err.name}` === 'NotFoundError') {
+        return undefined; // The user just clicked cancel.
+      }
+      throw err;
+    }
+    const usbiface = getAdbWebUsbInterface(usbdev);
+    if (usbiface === undefined) return undefined;
+
+    const key = usbDeviceToStr(usbdev);
+    this.removeTarget(key);
+
+    // If the user re-pairs the same device, remove it from the list and keep
+    // the new one.
+    const newTarget = new AdbWebusbTarget(usbiface, this.adbKeyMgr);
+    this.targets.set(key, newTarget);
+    this.onTargetsChanged.notify();
+    return newTarget;
+  }
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    if (!exists(navigator.usb)) {
+      yield {
+        name: 'WebUSB support',
+        status: errResult(`Not supported`),
+      };
+    }
+  }
+
+  private async refreshTargets() {
+    let triggerOnTrgetsChanged = false;
+    const usbDevices = await this.listUsbDevices();
+    // Find and disconnected devices.
+    for (const key of this.targets.keys()) {
+      if (!usbDevices.has(key)) {
+        // Entry disconnected.
+        this.removeTarget(key);
+        triggerOnTrgetsChanged = true;
+      }
+    }
+    for (const [key, usbiface] of usbDevices.entries()) {
+      if (this.targets.has(key)) continue; // We already have this target.
+      const newTarget = new AdbWebusbTarget(usbiface, this.adbKeyMgr);
+      this.targets.set(key, newTarget);
+      triggerOnTrgetsChanged = true;
+    }
+    triggerOnTrgetsChanged && this.onTargetsChanged.notify();
+  }
+
+  private removeTarget(key: string) {
+    const target = this.targets.get(key);
+    if (target === undefined) return;
+    this.targets.delete(key);
+    target.disconnect();
+  }
+
+  private async listUsbDevices(): Promise<Map<string, AdbUsbInterface>> {
+    const devices = new Map<string, AdbUsbInterface>();
+    // NOTE: getDevices() only returns the previously paired devices. It will
+    // not list connected devices that never got paired. In order to discover
+    // those we need to call navigator.usb.requestDevices() which prompts the
+    // "pair device" dialog. See pairNewTarget().
+    for (const dev of await navigator.usb.getDevices()) {
+      const usbiface = getAdbWebUsbInterface(dev);
+      if (usbiface === undefined) continue;
+      const key = usbDeviceToStr(dev);
+      devices.set(key, usbiface);
+    }
+    return devices;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts
new file mode 100644
index 0000000..cbf12db
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/adb_webusb_utils.ts
@@ -0,0 +1,73 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {exists} from '../../../../base/utils';
+
+export const ADB_DEVICE_FILTER = {
+  classCode: 255, // USB vendor specific code
+  subclassCode: 66, // Android vendor specific subclass
+  protocolCode: 1, // Adb protocol
+};
+
+export interface AdbUsbInterface {
+  readonly dev: USBDevice;
+  readonly configurationValue: number;
+  readonly usbInterfaceNumber: number;
+  readonly rx: number;
+  readonly tx: number;
+  readonly txPacketSize: number;
+}
+
+// Returns a key that can be used to index the device in a map for idempotency
+// checks.
+export function usbDeviceToStr(d: USBDevice): string {
+  const ver = `${d.deviceVersionMajor}.${d.deviceVersionMinor}`;
+  return `${d.vendorId}:${d.productId}:${ver}:${d.serialNumber}`;
+}
+
+export function getAdbWebUsbInterface(
+  device: USBDevice,
+): AdbUsbInterface | undefined {
+  if (!exists(device.serialNumber)) return undefined;
+  const adbDeviceFilter = ADB_DEVICE_FILTER;
+  for (const config of device.configurations) {
+    for (const iface of config.interfaces) {
+      for (const alt of iface.alternates) {
+        if (
+          alt.interfaceClass === adbDeviceFilter.classCode &&
+          alt.interfaceSubclass === adbDeviceFilter.subclassCode &&
+          alt.interfaceProtocol === adbDeviceFilter.protocolCode
+        ) {
+          const rxEndpoint = alt.endpoints.find(
+            (e) => e.type === 'bulk' && e.direction === 'in',
+          );
+          const txEndpoint = alt.endpoints.find(
+            (e) => e.type === 'bulk' && e.direction === 'out',
+          );
+          if (rxEndpoint === undefined || txEndpoint === undefined) continue;
+          return {
+            dev: device,
+            configurationValue: config.configurationValue,
+            usbInterfaceNumber: iface.interfaceNumber,
+            rx: rxEndpoint.endpointNumber,
+            tx: txEndpoint.endpointNumber,
+            txPacketSize: txEndpoint.packetSize,
+          };
+        } // if (alternate)
+      } // for (interface.alternates)
+    } // for (configuration.interfaces)
+  } // for (configurations)
+
+  return undefined;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/credentials_interfaces.d.ts
similarity index 100%
rename from ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
rename to ui/src/plugins/dev.perfetto.RecordTraceV2/adb/webusb/credentials_interfaces.d.ts
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts
new file mode 100644
index 0000000..76b1a7d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target.ts
@@ -0,0 +1,200 @@
+// 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 protos from '../../../protos';
+import {defer, Deferred} from '../../../base/deferred';
+import {errResult, okResult, Result} from '../../../base/result';
+import {binaryEncode} from '../../../base/string_utils';
+import {exists} from '../../../base/utils';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {ChromeExtensionTracingSession} from './chrome_extension_tracing_session';
+
+const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
+const EXTENSION_URL = `g.co/chrome/tracing-extension`;
+
+export class ChromeExtensionTarget implements RecordingTarget {
+  readonly id = 'chrome_extension';
+  readonly kind = 'LIVE_RECORDING';
+  readonly transportType = 'Extension';
+  platform: TargetPlatformId = 'CHROME';
+  private port?: chrome.runtime.Port;
+  private _connected = false;
+  private _extensionVersion?: string;
+  private _connectPromise?: Deferred<void>;
+  private chromeCategories?: string[];
+  private chromeCategoriesPromise = defer<string[]>();
+  private session?: ChromeExtensionTracingSession;
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    yield {
+      name: 'Tracing Extension',
+      status: await (async (): Promise<Result<string>> => {
+        if (!exists(window.chrome) || !exists(window.chrome.runtime)) {
+          return errResult(
+            'window.chrome.runtime not Available. ' +
+              'The extension is supported only in the Chrome browser',
+          );
+        }
+        try {
+          await this.connectIfNeeded();
+        } catch {}
+        return this._connected
+          ? okResult(`Connected (version: ${this._extensionVersion})`)
+          : errResult(`Not found. Please install ${EXTENSION_URL}`);
+      })(),
+    };
+
+    if (this.platform === 'CHROME_OS') {
+      yield {
+        name: 'CrOS detection',
+        status: ((): Result<string> => {
+          const userAgent = navigator.userAgent;
+          const isChromeOS = /CrOS/.test(userAgent);
+          return isChromeOS ? okResult(userAgent) : errResult(userAgent);
+        })(),
+      };
+    }
+  }
+
+  async connectIfNeeded(): Promise<void> {
+    if (!exists(window.chrome) || !exists(window.chrome.runtime)) {
+      return;
+    }
+    if (this._connected) return;
+    this.port = window.chrome.runtime.connect(EXTENSION_ID);
+    this.port.onMessage.addListener(this.onExtensionMessage.bind(this));
+    this.port.onDisconnect.addListener(this.onExtensionDisconnect.bind(this));
+
+    // This promise is resolved once the extension replies with 'version'.
+    // Unfortunately the chrome.runtime API doesn't offer a way to tell if the
+    // extension exists or not. The port is always connected. If the extension
+    // doesn't exist, then we receive an onDisconnect soon after.
+    const retPromise = defer<void>();
+    this._connectPromise = retPromise;
+
+    // This will trigger a promise resolution once the extension replies with
+    // the version (in onExtensionMessage() below);
+    this.invokeExtensionMethod('ExtensionVersion');
+    await retPromise;
+  }
+
+  disconnect(): void {
+    this._connected = false;
+    this.port?.disconnect();
+    this.port = undefined;
+  }
+
+  get connected(): boolean {
+    return this._connected;
+  }
+
+  get name(): string {
+    return 'Chrome (this browser)';
+  }
+
+  get emitsCompressedtrace(): boolean {
+    return this.platform === 'CHROME';
+  }
+
+  async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+    const categories = await this.getChromeCategories();
+    return okResult(categoriesToServiceState(categories));
+  }
+
+  async getChromeCategories(): Promise<string[]> {
+    if (this.chromeCategories === undefined) {
+      await this.connectIfNeeded();
+      this.chromeCategories = await this.chromeCategoriesPromise;
+    }
+    return this.chromeCategories;
+  }
+
+  async startTracing(
+    traceConfig: protos.ITraceConfig,
+  ): Promise<Result<ChromeExtensionTracingSession>> {
+    await this.connectIfNeeded();
+    if (!this._connected) {
+      return errResult('Cannot connect to the Chrome Tracing extension');
+    }
+    this.session = new ChromeExtensionTracingSession(this, traceConfig);
+    return okResult(this.session);
+  }
+
+  private onExtensionMessage(msg: object): void {
+    if ('version' in msg) {
+      this._connected = true;
+      this._extensionVersion = `${msg.version}`;
+      const cp = this._connectPromise;
+      this._connectPromise = undefined;
+      cp?.resolve();
+      this.invokeExtensionMethod('GetCategories');
+      return;
+    }
+
+    if (!('type' in msg)) {
+      return;
+    }
+
+    if (msg.type === 'GetCategoriesResponse') {
+      const cats = (msg as {type: string; categories: string[]}).categories;
+      this.chromeCategoriesPromise.resolve(cats);
+    } else {
+      this.session?.onExtensionMessage(`${msg.type}`, msg);
+    }
+  }
+
+  invokeExtensionMethod(method: string, data?: Uint8Array) {
+    const requestData = binaryEncode(data ?? new Uint8Array());
+    this.port?.postMessage({method, requestData});
+  }
+
+  private onExtensionDisconnect() {
+    if (this._connected) {
+      console.log(
+        'Chrome tracing extension disconnected',
+        chrome.runtime.lastError,
+      );
+    }
+    void chrome.runtime.lastError;
+    this.port = undefined;
+    this._connected = false;
+    if (this._connectPromise) {
+      this._connectPromise.reject('Chrome Tracing extension not found');
+    }
+    m.redraw();
+  }
+}
+
+function categoriesToServiceState(
+  categories: string[],
+): protos.ITracingServiceState {
+  return {
+    producers: [{id: 1, name: 'Chrome'}],
+    dataSources: [
+      {
+        producerId: 1,
+        dsDescriptor: {
+          name: 'track_event',
+          id: 1,
+          trackEventDescriptor: {
+            availableCategories: categories.map((cat) => ({name: cat})),
+          },
+        },
+      },
+    ],
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts
new file mode 100644
index 0000000..5e680c6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_target_provider.ts
@@ -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.
+
+import {EvtSource} from '../../../base/events';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {ChromeExtensionTarget} from './chrome_extension_target';
+
+export class ChromeExtensionTargetProvider implements RecordingTargetProvider {
+  readonly id = 'chrome_extension';
+  readonly name = 'Chrome Tracing extension';
+  readonly icon = 'extension';
+  readonly description = 'Chrome using extension';
+  readonly supportedPlatforms = ['CHROME', 'CHROME_OS'] as const;
+  readonly onTargetsChanged = new EvtSource<void>();
+
+  private target = new ChromeExtensionTarget();
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {}
+
+  async listTargets(
+    platform: TargetPlatformId,
+  ): Promise<ChromeExtensionTarget[]> {
+    this.target.platform = platform;
+    return [this.target];
+  }
+
+  getChromeCategories(): Promise<string[]> {
+    return this.target.getChromeCategories();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts
new file mode 100644
index 0000000..400bf3a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/chrome/chrome_extension_tracing_session.ts
@@ -0,0 +1,148 @@
+// 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 protos from '../../../protos';
+import {EvtSource} from '../../../base/events';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {binaryDecode} from '../../../base/string_utils';
+import {
+  TracingSession,
+  TracingSessionLogEntry,
+  TracingSessionState,
+} from '../interfaces/tracing_session';
+import {ChromeExtensionTarget} from './chrome_extension_target';
+import {defer, Deferred} from '../../../base/deferred';
+
+export class ChromeExtensionTracingSession implements TracingSession {
+  private _state: TracingSessionState = 'RECORDING';
+  readonly logs = new Array<TracingSessionLogEntry>();
+  private traceBuf = new ResizableArrayBuffer(64 * 1024);
+  readonly onSessionUpdate = new EvtSource<void>();
+  private pendingBufferUsage = new Array<Deferred<number>>();
+
+  constructor(
+    private target: ChromeExtensionTarget,
+    traceConfig: protos.ITraceConfig,
+  ) {
+    this.start(traceConfig);
+  }
+
+  private async start(traceConfig: protos.ITraceConfig): Promise<void> {
+    const requestData = protos.EnableTracingRequest.encode({
+      traceConfig,
+    }).finish();
+    this.target.invokeExtensionMethod('EnableTracing', requestData);
+  }
+
+  async stop(): Promise<void> {
+    this.target.invokeExtensionMethod('DisableTracing');
+    this.setState('STOPPING');
+  }
+
+  async cancel(): Promise<void> {
+    this.target.invokeExtensionMethod('FreeBuffers');
+    this.setState('STOPPING');
+  }
+
+  async getBufferUsagePct(): Promise<number | undefined> {
+    if (this._state !== 'RECORDING') return undefined;
+    const promise = defer<number>();
+    this.pendingBufferUsage.push(promise);
+    this.target.invokeExtensionMethod('GetTraceStats');
+    return promise;
+  }
+
+  getTraceData(): Uint8Array | undefined {
+    if (this._state !== 'FINISHED') return undefined;
+    const buf = this.traceBuf.get();
+    return buf;
+  }
+
+  onExtensionMessage(msgType: string, msg: object) {
+    switch (msgType) {
+      case 'ChromeExtensionError':
+        const err = (msg as {type: string; error: string}).error;
+        this.log(`Tracing failed: ${err}`, /* isError */ true);
+        if (this._state !== 'FINISHED') {
+          // Ignore spurious errors that arrive after the session finishes.
+          this.setState('ERRORED');
+          this.target.disconnect();
+        }
+        break;
+
+      case 'ChromeExtensionStatus':
+        const status = (msg as {type: string; status: string}).status;
+        this.log(status);
+        break;
+
+      case 'EnableTracingResponse':
+        this.target.invokeExtensionMethod('ReadBuffers');
+        this.setState('STOPPING');
+        break;
+
+      case 'GetTraceStatsResponse':
+        const statResp = msg as {type: string} & protos.IGetTraceStatsResponse;
+        let totSize = 0;
+        let usedSize = 0;
+        for (const buf of statResp.traceStats?.bufferStats ?? []) {
+          totSize += buf.bufferSize ?? 0;
+          // bytesWritten can be >> bufferSize for ring buffer traces.
+          usedSize += Math.min(buf.bytesWritten ?? 0, buf.bufferSize ?? 0);
+        }
+        const pct = Math.min(Math.round((100 * usedSize) / totSize), 100);
+        for (const promise of this.pendingBufferUsage.splice(0)) {
+          promise.resolve(pct);
+        }
+        break;
+
+      case 'ReadBuffersResponse':
+        // The extension is really misusing the ReadBuffersResponse:
+        // - Data is a binary string, not a Uint8Array
+        // - The field 'lastSliceForPacket' is really 'lastPacketInTrace'.
+        // - Slices are really packets and don't need preambles.
+        // See http://shortn/_53WB8A1aIr.
+        const resp = msg as {type: string} & protos.IReadBuffersResponse;
+        let eof = false;
+        for (const slice of resp.slices ?? []) {
+          const data = binaryDecode(slice.data as unknown as string);
+          this.traceBuf.append(data);
+          eof = Boolean(slice.lastSliceForPacket);
+          if (eof) {
+            this.setState('FINISHED');
+            this.target.invokeExtensionMethod('FreeBuffers');
+            break;
+          }
+        }
+        break;
+    }
+  }
+
+  get state(): TracingSessionState {
+    return this._state;
+  }
+
+  private setState(newState: TracingSessionState) {
+    this._state = newState;
+    this.onSessionUpdate.notify();
+  }
+
+  private log(message: string, isError = false) {
+    this.logs.push({
+      message,
+      timestamp: new Date(),
+      isError,
+    });
+    this.onSessionUpdate.notify();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts
new file mode 100644
index 0000000..41db0ac
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_interfaces.ts
@@ -0,0 +1,140 @@
+// 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 {TargetPlatformId} from '../interfaces/target_platform';
+import {TraceConfigBuilder} from './trace_config_builder';
+import {RecordPluginSchema, RecordSessionSchema} from '../serialization_schema';
+
+/**
+ * A sub-page of the Record page.
+ * Each section maps to an entry in the left sidebar of the recording page.
+ * There are three types of subpages. The last two are identical with exception
+ * of the serialization scope.
+ * 1. Probes pages: the ones that are a structured collection of probes that
+ *    can be toggled.
+ * 2,3. Session and global pages: they care of their own rendering and
+ *    de/serialization.
+ * 2. Session pages serialize their state in the per-session object (e.g. buffer
+ *    sizes). This object can be shared with other people when using the share
+ *    config feature.
+ * 3. Global pages instead hold onto the "global" state of the plugin, which is
+ *    not tied to the specific config of the recording session (e.g. the target
+ *    being recorded, the list of saved configs). This state is retained in
+ *    localstorage but is NOT shared with others.
+ */
+export type RecordSubpage = {
+  /** A unique string. This becomes the subpage in the fragment #!/record/xxx */
+  readonly id: string;
+
+  /** The name of the material-design icon that is displayed on the sidebar. */
+  readonly icon: string;
+
+  /** The main text displayed in the left sidebar. */
+  readonly title: string;
+
+  /** The subtitle displayed when hovering over the entry of the sidebar. */
+  readonly subtitle: string;
+} & (
+  | {
+      kind: 'PROBES_PAGE';
+
+      /** The list of probes (togglable entries) for this section. */
+      readonly probes: ReadonlyArray<RecordProbe>;
+    }
+  | {
+      kind: 'SESSION_PAGE';
+      render(): m.Children;
+
+      // Save-restore the page state into the JSON object that is saved in
+      // localstorage and shared when sharing a config.
+      serialize(state: RecordSessionSchema): void;
+      deserialize(state: RecordSessionSchema): void;
+    }
+  | {
+      kind: 'GLOBAL_PAGE';
+      render(): m.Children;
+
+      // Save-restore the page state into the JSON object that is saved in
+      // localstorage.
+      serialize(state: RecordPluginSchema): void;
+      deserialize(state: RecordPluginSchema): void;
+    }
+);
+
+export interface RecordProbe {
+  /**
+   * lower_with_under id. Keep stable, is used for serialization.
+   * This id must be globally unique (not just per-section).
+   */
+  readonly id: string;
+
+  /** Human readable name. */
+  readonly title: string;
+
+  /** (optional) decription. */
+  readonly description?: string;
+
+  /** (optional) file name of a .png file under assets/. */
+  readonly image?: string;
+
+  /** (optional) Link to documentation (e.g. 'https://docs.perfetto.dev/...') */
+  readonly docsLink?: string;
+
+  /** (optional). If specified restricts the probe to the given platorms. */
+  readonly supportedPlatforms?: TargetPlatformId[];
+
+  /** (optional): a list of settings for the probe (e.g. polling interval). */
+  readonly settings?: Record<string, ProbeSetting>;
+
+  /**
+   * (optional): a list of probe IDs that will be force-enabled if this probe is
+   * also enabled.
+   */
+  readonly dependencies?: string[];
+
+  /**
+   * Generate the TraceConfig for the probe. This happens in vdom-style: every
+   * time we make a change to the probes the RecordingManager starts a blank
+   * TraceConfigBuilder and asks all probes to update its config invoking this
+   * method.
+   */
+  genConfig(tc: TraceConfigBuilder): void;
+}
+
+/**
+ * The interface to create widgets that change the state of a probe.
+ * The widget is maintains its own state and must be able to de/serialize it.
+ * Realistically you don't want to implment this interface yourself but use one
+ * of the pre-made widgets under ../pages/widgets/, e.g., Slider().
+ */
+export interface ProbeSetting {
+  readonly render: () => m.Children;
+
+  // The two methods below are supposed to save/restore the state of the setting
+  // in a JSON-serializable entity (object | number | string | boolean). This is
+  // to support saving configs into localstorage and sharing them.
+  serialize(): unknown;
+  deserialize(state: unknown): void;
+}
+
+export function supportsPlatform(
+  probe: RecordProbe,
+  platform: TargetPlatformId,
+): boolean {
+  return (
+    probe.supportedPlatforms === undefined ||
+    probe.supportedPlatforms.includes(platform)
+  );
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts
new file mode 100644
index 0000000..32c6af7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/config_manager.ts
@@ -0,0 +1,180 @@
+// 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 protos from '../../../protos';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {getOrCreate} from '../../../base/utils';
+import {ProbesSchema} from '../serialization_schema';
+import {TargetPlatformId} from '../interfaces/target_platform';
+import {RecordProbe, supportsPlatform} from './config_interfaces';
+import {DEFAULT_BUFFER_ID, TraceConfigBuilder} from './trace_config_builder';
+
+/**
+ * ConfigManager holds all the state required for config generation (everything
+ * in the record page that has nothing to do with the actual record over
+ * webusb/websocket).
+ * Recording is arranged as a set of Probes. A Probe is a slightly different
+ * concept than a DataSource, as in, it's a higher-level, more user-friendly
+ * concept to help the user configuring tracing behaviours with toggles.
+ * In some cases a Probe can just match 1:1 with a data source; in other cases
+ * N probes can contribute to the same data source (e.g. when they enable
+ * different ftrace events); In other cases a probe can enable 2+ data sources.
+ * At the end of the day, probe contribute to generating a TraceConfig proto.
+ * They do so in a react-style fashion (we start from blank and append entries
+ * every time there is a change). @see {@link TraceConfigBuilder}.
+ */
+export class ConfigManager {
+  readonly probesById = new Map<string, RecordProbe>();
+  private _traceConfig = new TraceConfigBuilder();
+  private enabledProbes = new Map<string, boolean>();
+  private indirectlyEnabledProbes = new Map<string, Set<string>>();
+
+  get traceConfig() {
+    return this._traceConfig;
+  }
+
+  registerProbes(probes: ReadonlyArray<RecordProbe>) {
+    for (const probe of probes) {
+      assertFalse(this.probesById.has(probe.id));
+      this.probesById.set(probe.id, probe);
+    }
+  }
+
+  setProbeEnabled(probeId: string, enabled: boolean) {
+    const probe = assertExists(this.probesById.get(probeId));
+    this.enabledProbes.set(probeId, enabled);
+    for (const depProbeId of probe.dependencies ?? []) {
+      assertTrue(this.probesById.has(depProbeId));
+      const depSet = getOrCreate(
+        this.indirectlyEnabledProbes,
+        depProbeId,
+        () => new Set<string>(),
+      );
+      if (enabled) {
+        depSet.add(probeId);
+      } else {
+        depSet.delete(probeId);
+      }
+    }
+  }
+
+  isProbeEnabled(probeId: string): boolean {
+    const directlyEnabled = this.enabledProbes.get(probeId) === true;
+    const enabledDueToDeps = Boolean(
+      this.indirectlyEnabledProbes.get(probeId)?.size,
+    );
+    return directlyEnabled || enabledDueToDeps;
+  }
+
+  /**
+   * Returns the human-friendly name for the probes that are enabled and depend
+   * on this probe. This is so we can tell the user: you cannot turn this probe
+   * off because another probe you enbled requires this one.
+   */
+  getProbeEnableDependants(probeId: string): string[] {
+    return Array.from(this.indirectlyEnabledProbes.get(probeId) ?? []).map(
+      (id) => assertExists(this.probesById.get(id)).title,
+    );
+  }
+
+  /**
+   * Generates the TraceConfig proto for the current configuration.
+   */
+  genTraceConfig(platform: TargetPlatformId): protos.TraceConfig {
+    // We approach trace config generation similar to vdom rendering: we start
+    // fresh all the time and let the various probes add things to the
+    // TraceConfigBuilder.
+
+    this._traceConfig.dataSources.clear();
+
+    // Clear all buffers other than the default one.
+    for (const bufId of this._traceConfig.buffers.keys()) {
+      if (bufId !== DEFAULT_BUFFER_ID) {
+        this._traceConfig.buffers.delete(bufId);
+      }
+    }
+
+    // Now regenerate the config. Go in probe registration order, but
+    // respect dependencies (deps come first).
+    const orderedProbes = this.getProbesOrderedByDep(/* enabledOnly */ true);
+
+    for (const probe of orderedProbes) {
+      if (!supportsPlatform(probe, platform)) continue;
+      probe.genConfig(this._traceConfig);
+    }
+    return this._traceConfig.toTraceConfig();
+  }
+
+  // For sharing and localstorage persistence.
+  serializeProbes(): ProbesSchema {
+    return Object.fromEntries(
+      this.getProbesOrderedByDep(/* enabledOnly */ true).map((probe) => [
+        probe.id,
+        {
+          settings: Object.fromEntries(
+            Object.entries(probe.settings ?? {}).map(([settingId, setting]) => [
+              settingId,
+              setting.serialize(),
+            ]),
+          ),
+        },
+      ]),
+    );
+  }
+
+  // For sharing and localstorage persistence.
+  deserializeProbes(state: ProbesSchema): void {
+    this.enabledProbes.clear();
+    this.indirectlyEnabledProbes.clear();
+    this.getProbesOrderedByDep().forEach((probe) => {
+      const probeState = state[probe.id];
+      if (probeState === undefined || probeState.settings === undefined) {
+        return;
+      }
+      this.setProbeEnabled(probe.id, true);
+      if (probe.settings === undefined) {
+        // The probe has no settings, there is nothing to restore.
+        // This return is theoretically redundant but is here to make tsc happy.
+        return;
+      }
+      for (const [key, settingState] of Object.entries(probeState.settings)) {
+        if (key in probe.settings) {
+          probe.settings[key].deserialize(settingState);
+        }
+      }
+    });
+  }
+
+  private getProbesOrderedByDep(enabledOnly = false): RecordProbe[] {
+    const orderedProbes: RecordProbe[] = [];
+    const seenIds = new Set<string>();
+    const queueProbe = (probeId: string) => {
+      if (enabledOnly && !this.isProbeEnabled(probeId)) return;
+      const probe = assertExists(this.probesById.get(probeId));
+      if (orderedProbes.includes(probe)) return; // Already added.
+      if (seenIds.has(probeId)) {
+        throw new Error('Cycle detected in probe ' + probeId);
+      }
+      seenIds.add(probeId);
+      for (const dep of probe.dependencies ?? []) {
+        queueProbe(dep);
+      }
+      orderedProbes.push(probe);
+    };
+    for (const probeId of this.probesById.keys()) {
+      queueProbe(probeId);
+    }
+    return orderedProbes;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts
new file mode 100644
index 0000000..0491cb6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_builder.ts
@@ -0,0 +1,130 @@
+// 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 {assertExists, assertFalse} from '../../../base/logging';
+import {getOrCreate} from '../../../base/utils';
+import protos from '../../../protos';
+
+export const FTRACE_DS = 'linux.ftrace';
+export type RecordMode = 'STOP_WHEN_FULL' | 'RING_BUFFER' | 'LONG_TRACE';
+export type BufferMode = 'DISCARD' | 'RING_BUFFER';
+
+export const DEFAULT_BUFFER_ID = 'default';
+
+export class TraceConfigBuilder {
+  readonly buffers = new Map<string, BufferConfig>();
+  readonly dataSources = new Map<string, DataSource>();
+
+  // The default values here don't matter, they exist only to make the TS
+  // compiler happy. The actual defaults are defined by serialization_schema.ts.
+  mode: RecordMode = 'STOP_WHEN_FULL';
+  durationMs = 10_000;
+  maxFileSizeMb = 0;
+  fileWritePeriodMs = 0;
+  compression = false;
+
+  constructor() {
+    this.buffers.set(DEFAULT_BUFFER_ID, {sizeKb: 64 * 1024});
+  }
+
+  get defaultBuffer(): BufferConfig {
+    return assertExists(this.buffers.get(DEFAULT_BUFFER_ID));
+  }
+
+  // It has get-or-create semantics.
+  addDataSource(name: string, targetBufId?: string): protos.IDataSourceConfig {
+    return getOrCreate(this.dataSources, name, () => ({
+      targetBufId,
+      config: {name},
+    })).config;
+  }
+
+  addBuffer(id: string, sizeKb: number, mode?: BufferMode) {
+    assertFalse(this.buffers.has(id));
+    this.buffers.set(id, {sizeKb, mode});
+  }
+
+  addFtraceEvents(...ftraceEvents: string[]) {
+    const cfg = this.addDataSource('linux.ftrace');
+    cfg.ftraceConfig ??= {};
+    cfg.ftraceConfig.ftraceEvents ??= [];
+    cfg.ftraceConfig.ftraceEvents.push(...ftraceEvents);
+  }
+
+  addAtraceApps(...apps: string[]) {
+    const cfg = this.addDataSource('linux.ftrace');
+    cfg.ftraceConfig ??= {};
+    cfg.ftraceConfig.atraceApps ??= [];
+    cfg.ftraceConfig.atraceApps.push(...apps);
+  }
+
+  addAtraceCategories(...cats: string[]) {
+    const cfg = this.addDataSource('linux.ftrace');
+    cfg.ftraceConfig ??= {};
+    cfg.ftraceConfig.atraceCategories ??= [];
+    cfg.ftraceConfig.atraceCategories.push(...cats);
+  }
+
+  toTraceConfig(): protos.TraceConfig {
+    const traceCfg = new protos.TraceConfig();
+    traceCfg.durationMs = this.durationMs;
+    if (this.mode === 'LONG_TRACE') {
+      traceCfg.writeIntoFile = true;
+      traceCfg.fileWritePeriodMs = this.fileWritePeriodMs;
+      traceCfg.maxFileSizeBytes = this.maxFileSizeMb * 1_000_000;
+    }
+
+    if (this.compression) {
+      traceCfg.compressionType =
+        protos.TraceConfig.CompressionType.COMPRESSION_TYPE_DEFLATE;
+    }
+
+    const orderedBufIds = [];
+    for (const [id, buf] of this.buffers.entries()) {
+      const fillPolicy =
+        buf.mode === 'DISCARD' ||
+        (buf.mode === undefined && this.mode === 'STOP_WHEN_FULL')
+          ? protos.TraceConfig.BufferConfig.FillPolicy.DISCARD
+          : protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER;
+      traceCfg.buffers.push({sizeKb: buf.sizeKb, fillPolicy});
+      orderedBufIds.push(id);
+    }
+    for (const ds of this.dataSources.values()) {
+      let targetBuffer: number | undefined = undefined;
+      if (ds.targetBufId !== undefined) {
+        targetBuffer = orderedBufIds.indexOf(ds.targetBufId);
+        if (targetBuffer < 0) {
+          throw new Error(
+            `DataSource ${ds.config.name} specified buffer id ` +
+              `${ds.targetBufId} but it doesn't exist. ` +
+              `Buffers: [${orderedBufIds.join(',')}]`,
+          );
+        }
+      }
+      traceCfg.dataSources.push({config: {...ds.config, targetBuffer}});
+    }
+    return traceCfg;
+  }
+}
+
+export interface DataSource {
+  config: protos.IDataSourceConfig;
+  targetBufId?: string;
+}
+
+export interface BufferConfig {
+  sizeKb: number;
+  // If omitted infers from the config-wide mode.
+  mode?: BufferMode;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts
new file mode 100644
index 0000000..6027ddf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/config/trace_config_utils_wasm.ts
@@ -0,0 +1,113 @@
+// 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 protos from '../../../protos';
+import {assetSrc} from '../../../base/assets';
+import {defer} from '../../../base/deferred';
+import {assertTrue} from '../../../base/logging';
+import {errResult, okResult, Result} from '../../../base/result';
+import {utf8Decode, utf8Encode} from '../../../base/string_utils';
+import WasmModuleGen from '../../../gen/trace_config_utils';
+
+/**
+ * This file is the TS-equivalent of src/trace_config_utils.
+ * It exposes two functions to conver the TraceConfig proto from txt<>protobuf.
+ * It guarrantees to have the same behaviour of perfetto_cmd and trace_processor
+ * by using precisely the same code via WebAssembly.
+ */
+interface WasmModule {
+  module: WasmModuleGen.Module;
+  buf: Uint8Array;
+}
+
+let moduleInstance: WasmModule | undefined = undefined;
+
+/**
+ * Convert a binary-encoded protos.TracConfig to pbtxt (i.e. the text format
+ * that can be passed to perfetto --txt).
+ */
+export async function traceConfigToTxt(
+  config: Uint8Array | protos.ITraceConfig,
+): Promise<string> {
+  const wasm = await initWasmOnce();
+
+  const configU8: Uint8Array =
+    config instanceof Uint8Array
+      ? config
+      : protos.TraceConfig.encode(config).finish();
+  assertTrue(configU8.length <= wasm.buf.length);
+  wasm.buf.set(configU8);
+
+  const txtSize =
+    wasm.module.ccall(
+      'trace_config_pb_to_txt',
+      'number',
+      ['number'],
+      [configU8.length],
+    ) >>> 0;
+
+  const txt = utf8Decode(wasm.buf.subarray(0, txtSize));
+  return txt;
+}
+
+/** Convert a pbtxt (text-proto) text to a proto-encoded TraceConfig. */
+export async function traceConfigToPb(
+  configTxt: string,
+): Promise<Result<Uint8Array>> {
+  const wasm = await initWasmOnce();
+
+  const configUtf8 = utf8Encode(configTxt);
+  assertTrue(configUtf8.length <= wasm.buf.length);
+  wasm.buf.set(configUtf8);
+
+  const resSize =
+    wasm.module.ccall(
+      'trace_config_txt_to_pb',
+      'number',
+      ['number'],
+      [configUtf8.length],
+    ) >>> 0;
+
+  const success = wasm.buf.at(0) === 1;
+  const payload = wasm.buf.slice(1, 1 + resSize);
+  return success ? okResult(payload) : errResult(utf8Decode(payload));
+}
+
+async function initWasmOnce(): Promise<WasmModule> {
+  if (moduleInstance === undefined) {
+    // We have to fetch the .wasm file manually because the stub generated by
+    // emscripten uses sync-loading, which works only in Workers.
+    const resp = await fetch(assetSrc('trace_config_utils.wasm'));
+    const wasmBinary = await resp.arrayBuffer();
+    const deferredRuntimeInitialized = defer<void>();
+    const instance = WasmModuleGen({
+      noInitialRun: true,
+      locateFile: (s: string) => s,
+      print: (s: string) => console.log(s),
+      printErr: (s: string) => console.error(s),
+      onRuntimeInitialized: () => deferredRuntimeInitialized.resolve(),
+      wasmBinary,
+    } as WasmModuleGen.ModuleArgs);
+    await deferredRuntimeInitialized;
+    const bufAddr =
+      instance.ccall('trace_config_utils_buf', 'number', [], []) >>> 0;
+    const bufSize =
+      instance.ccall('trace_config_utils_buf_size', 'number', [], []) >>> 0;
+    moduleInstance = {
+      module: instance,
+      buf: instance.HEAPU8.subarray(bufAddr, bufAddr + bufSize),
+    };
+  }
+  return moduleInstance;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts
new file mode 100644
index 0000000..48ec477
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/index.ts
@@ -0,0 +1,92 @@
+// 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 {bindMithrilAttrs} from '../../base/mithril_utils';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import RecordingV1Plugin from '../dev.perfetto.RecordTrace';
+import {AdbWebsocketTargetProvider} from './adb/websocket/adb_websocket_target_provider';
+import {AdbWebusbTargetProvider} from './adb/webusb/adb_webusb_target_provider';
+import {ChromeExtensionTargetProvider} from './chrome/chrome_extension_target_provider';
+import {advancedRecordSection} from './pages/advanced';
+import {androidRecordSection} from './pages/android';
+import {bufferConfigPage} from './pages/buffer_config_page';
+import {chromeRecordSection} from './pages/chrome';
+import {instructionsPage} from './pages/instructions_page';
+import {cpuRecordSection} from './pages/cpu';
+import {gpuRecordSection} from './pages/gpu';
+import {memoryRecordSection} from './pages/memory';
+import {powerRecordSection} from './pages/power';
+import {RecordPageV2} from './pages/record_page';
+import {stackSamplingRecordSection} from './pages/stack_sampling';
+import {targetSelectionPage} from './pages/target_selection_page';
+import {RecordingManager} from './recording_manager';
+import {TracedWebsocketTargetProvider} from './traced_over_websocket/traced_websocket_provider';
+import {savedConfigsPage} from './pages/saved_configs';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.RecordTraceV2';
+  static readonly dependencies = [RecordingV1Plugin];
+  private static recordingMgr?: RecordingManager;
+
+  static onActivate(app: App) {
+    if (!RecordingV1Plugin.useRecordingV2) return;
+    app.sidebar.addMenuItem({
+      section: 'navigation',
+      text: 'Record new trace',
+      href: '#!/record',
+      icon: 'fiber_smart_record',
+      sortOrder: 2,
+    });
+    app.pages.registerPage({
+      route: '/record',
+      traceless: true,
+      page: bindMithrilAttrs(RecordPageV2, {
+        getRecordingManager: this.getRecordingManager.bind(this, app),
+      }),
+    });
+  }
+
+  // Lazily initialize the RecordingManager at first call. This is to prevent
+  // providers to connect to sockets / devtools (which in turn can trigger
+  // security UX in the browser) before the user has even done anything.
+  private static getRecordingManager(app: App): RecordingManager {
+    if (this.recordingMgr === undefined) {
+      const recMgr = new RecordingManager(app);
+      this.recordingMgr = recMgr;
+      recMgr.registerProvider(new AdbWebusbTargetProvider());
+      recMgr.registerProvider(new AdbWebsocketTargetProvider());
+      const chromeProvider = new ChromeExtensionTargetProvider();
+      recMgr.registerProvider(chromeProvider);
+      recMgr.registerProvider(new TracedWebsocketTargetProvider());
+      recMgr.registerPage(
+        targetSelectionPage(recMgr),
+        bufferConfigPage(recMgr),
+        instructionsPage(recMgr),
+        savedConfigsPage(recMgr),
+
+        chromeRecordSection(() => chromeProvider.getChromeCategories()),
+        cpuRecordSection(),
+        gpuRecordSection(),
+        powerRecordSection(),
+        memoryRecordSection(),
+        androidRecordSection(),
+        stackSamplingRecordSection(),
+        advancedRecordSection(),
+      );
+      recMgr.restorePluginStateFromLocalstorage();
+    }
+    return this.recordingMgr;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts
new file mode 100644
index 0000000..7e165fe
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/byte_stream.ts
@@ -0,0 +1,28 @@
+// 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.
+
+/**
+ * The base class for implementing byte streams. This is used both for
+ * implementing various layers of the ADB stack and for modelling data exhanges
+ * on the tracing protocol.
+ */
+export abstract class ByteStream {
+  // Event handlers
+  onData: (data: Uint8Array) => void = () => {};
+  onClose: () => void = () => {};
+
+  abstract get connected(): boolean;
+  abstract write(data: string | Uint8Array): Promise<void>;
+  abstract close(): void;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts
new file mode 100644
index 0000000..b226844
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/connection_check.ts
@@ -0,0 +1,47 @@
+// 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 {Result} from '../../../base/result';
+
+export interface WithPreflightChecks {
+  /**
+   * yields a sequence of diagnostic check that should be performed before
+   * starting the connection. Those checks provide actionable information about
+   * missed preconditions.
+   */
+  runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+}
+
+export type PreflightCheckResult = Result<string>;
+
+export interface PreflightCheck {
+  /** E.g. "Check Android Version", "Check WebUSB Connection" */
+  readonly name: string;
+
+  /**
+   * 1. An OK status, if the check succeeds. In this case the value can carry a
+   *    message (e.g. "Connected, version 1.2.3").
+   * 2. An Error status, alongside the message: (e.g. "Could not connect to
+   *    127.0.0.1:1234").
+   */
+  readonly status: PreflightCheckResult;
+
+  /**
+   * [Optional] A mithril component that shows instruction on how to remediate
+   * to the problem. For cases where the StatusOr error is not enough and we
+   * want to show something more interactive.
+   */
+  readonly remediation?: m.ComponentTypes;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts
new file mode 100644
index 0000000..f66260f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target.ts
@@ -0,0 +1,50 @@
+// 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 protos from '../../../protos';
+import {Result} from '../../../base/result';
+import {PreflightCheck, WithPreflightChecks} from './connection_check';
+import {TargetPlatformId} from './target_platform';
+import {TracingSession} from './tracing_session';
+
+/**
+ * The interface that models a device that can be used for recording a trace.
+ * This is the contract that RecordingTargetProvider(s) must implement in order
+ * to support recording. The UI bits don't care about the specific
+ * implementation and only use this class.
+ * Conceptually a RecordingTarget maps to a connection to the Consumer socket
+ * to the tracing service.
+ */
+export interface RecordingTarget extends WithPreflightChecks {
+  readonly id: string;
+  readonly platform: TargetPlatformId;
+  readonly name: string;
+  readonly transportType: string;
+  readonly connected: boolean;
+
+  // If true, the output file is gzip-compressed as a whole (!= than setting
+  // deflate in the trace config). The chrome devtools protocol does this.
+  readonly emitsCompressedtrace?: boolean;
+
+  // Returns a list of debugging check to diagnose target connection failures.
+  runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+
+  getServiceState(): Promise<Result<protos.ITracingServiceState>>;
+
+  disconnect(): void;
+
+  startTracing(
+    traceConfig: protos.ITraceConfig,
+  ): Promise<Result<TracingSession>>;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.ts
new file mode 100644
index 0000000..c41b45d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/recording_target_provider.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 {Evt} from '../../../base/events';
+import {PreflightCheck, WithPreflightChecks} from './connection_check';
+import {RecordingTarget} from './recording_target';
+import {TargetPlatformId} from './target_platform';
+
+/**
+ * The interface to describe target providers. A target provider uses a specific
+ * transport (e.g., WebUsb, WebSocket, Chrome extension) and allows to find
+ * and obtain Targets.
+ */
+export interface RecordingTargetProvider extends WithPreflightChecks {
+  readonly id: string;
+  readonly name: string;
+  readonly icon: string;
+  readonly description: string;
+  readonly supportedPlatforms: ReadonlyArray<TargetPlatformId>;
+
+  /**
+   * Event listener raised when the target list changes.
+   * The caller is expected to call listTargets() in response to this.
+   */
+  readonly onTargetsChanged: Evt<void>;
+
+  /** Returns a list of debugging checks to diagnose connection failures. */
+  runPreflightChecks(): AsyncGenerator<PreflightCheck>;
+
+  /**
+   * Lists the targets that can be discovered. Note that some providers
+   * (notably WebUSB) can't discover devices that never got paired before and
+   * need a call to {@link pairNewTarget()} to pop up a pair dialog.
+   */
+  listTargets(platform: TargetPlatformId): Promise<RecordingTarget[]>;
+
+  /**
+   * Optional. Some transports can't discover all targets upfront and need
+   * some user interaction to add a new target.
+   */
+  pairNewTarget?: () => Promise<RecordingTarget | undefined>;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts
new file mode 100644
index 0000000..db56278
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/target_platform.ts
@@ -0,0 +1,44 @@
+// 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.
+
+export interface TargetPlatform {
+  id: string;
+  name: string;
+  icon: string;
+}
+
+export const TARGET_PLATFORMS = [
+  {
+    id: 'ANDROID',
+    name: 'Android',
+    icon: 'android',
+  },
+  {
+    id: 'CHROME',
+    name: 'Chrome',
+    icon: 'travel_explore',
+  },
+  {
+    id: 'CHROME_OS',
+    name: 'ChromeOS',
+    icon: 'laptop_chromebook',
+  },
+  {
+    id: 'LINUX',
+    name: 'Linux',
+    icon: 'dns',
+  },
+] as const;
+
+export type TargetPlatformId = (typeof TARGET_PLATFORMS)[number]['id'];
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts
new file mode 100644
index 0000000..e404608
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/interfaces/tracing_session.ts
@@ -0,0 +1,49 @@
+// 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 {Evt} from '../../../base/events';
+import {RecordingTarget} from './recording_target';
+
+/**
+ * The contract for the object returned by {@link RecordingTarget.startTracing}.
+ */
+export interface TracingSession {
+  readonly state: TracingSessionState;
+  readonly logs: ReadonlyArray<TracingSessionLogEntry>;
+  readonly onSessionUpdate: Evt<void>;
+
+  /** Stop tracing and get the data captured so far. */
+  stop(): Promise<void>;
+
+  /** Stop tracing and discard the data. */
+  cancel(): Promise<void>;
+
+  /* Returns the percentage of the trace buffer that is currently used */
+  getBufferUsagePct(): Promise<number | undefined>;
+
+  /** Returns the trace file captured once state === 'FINISHED'. */
+  getTraceData(): Uint8Array | undefined;
+}
+
+export type TracingSessionState =
+  | 'RECORDING'
+  | 'STOPPING'
+  | 'FINISHED'
+  | 'ERRORED';
+
+export interface TracingSessionLogEntry {
+  readonly timestamp: Date;
+  readonly message: string;
+  readonly isError?: boolean;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts
new file mode 100644
index 0000000..93a9fa9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/advanced.ts
@@ -0,0 +1,154 @@
+// 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 {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {FTRACE_DS, TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {Slider} from './widgets/slider';
+import {Toggle} from './widgets/toggle';
+
+export const ADV_PROC_ASSOC_PROBE_ID = 'adv_proc_thread_assoc';
+export const PROC_STATS_DS_NAME = 'linux.process_stats';
+export const ADV_FTRACE_PROBE_ID = 'advanced_ftrace';
+
+export function advancedRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'advanced',
+    title: 'Advanced settings',
+    subtitle: 'For ftrace wizards',
+    icon: 'settings',
+    probes: [ftraceCfg(), procThreadAssociation()],
+  };
+}
+
+function ftraceCfg(): RecordProbe {
+  const settings = {
+    ksyms: new Toggle({
+      title: 'Resolve kernel symbols',
+      default: true,
+      descr:
+        'Enables lookup via /proc/kallsyms for workqueue, ' +
+        'sched_blocked_reason and other events ' +
+        '(userdebug/eng builds only).',
+    }),
+    genericEvents: new Toggle({
+      title: 'Enable generic events (slow)',
+      descr:
+        'Enables capture of ftrace events that are not known at build time ' +
+        'by perfetto as key-value string pairs. This is slow and expensive.',
+    }),
+    bufSize: new Slider({
+      title: 'Buf size',
+      cssClass: '.thin',
+      values: [0, 512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
+      unit: 'KB',
+      zeroIsDefault: true,
+    }),
+    drainRate: new Slider({
+      title: 'trace_pipe_raw read interval',
+      cssClass: '.thin',
+      values: [0, 100, 250, 500, 1000, 2500, 5000],
+      unit: 'ms',
+      zeroIsDefault: true,
+    }),
+    groups: new TypedMultiselect<string>({
+      title: 'Event groups',
+      options: new Map(
+        Object.entries({
+          binder: 'binder/*',
+          block: 'block/*',
+          clk: 'clk/*',
+          ext4: 'ext4/*',
+          f2fs: 'f2fs/*',
+          i2c: 'i2c/*',
+          irq: 'irq/*',
+          kmem: 'kmem/*',
+          memory_bus: 'memory_bus/*',
+          mmc: 'mmc/*',
+          oom: 'oom/*',
+          power: 'power/*',
+          regulator: 'regulator/*',
+          sched: 'sched/*',
+          sync: 'sync/*',
+          task: 'task/*',
+          vmscan: 'vmscan/*',
+          fastrpc: 'fastrpc/*',
+        }),
+      ),
+    }),
+  };
+  return {
+    id: ADV_FTRACE_PROBE_ID,
+    title: 'Advanced ftrace config',
+    image: 'rec_ftrace.png',
+    description:
+      'Enable individual events and tune the kernel-tracing (ftrace) ' +
+      'module. The events enabled here are in addition to those from ' +
+      'enabled by other probes.',
+    supportedPlatforms: ['ANDROID', 'CHROME_OS', 'LINUX'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const ds = tc.addDataSource(FTRACE_DS);
+      const cfg = (ds.ftraceConfig ??= {});
+      cfg.bufferSizeKb = settings.bufSize.value || undefined;
+      cfg.drainPeriodMs = settings.drainRate.value || undefined;
+      cfg.symbolizeKsyms = settings.ksyms.enabled ? true : undefined;
+      cfg.disableGenericEvents = !settings.genericEvents.enabled;
+      cfg.ftraceEvents ??= [];
+      cfg.ftraceEvents.push(...settings.groups.selectedValues());
+    },
+  };
+}
+
+function procThreadAssociation(): RecordProbe {
+  const ftraceEvents = [
+    'sched/sched_process_exit',
+    'sched/sched_process_free',
+    'task/task_newtask',
+    'task/task_rename',
+  ];
+  const settings = {
+    initialScan: new Toggle({
+      title: 'Scan all processes at startup',
+      descr: 'Reports all /proc/* processes when starting',
+      default: true,
+    }),
+  };
+  return {
+    id: ADV_PROC_ASSOC_PROBE_ID,
+    title: 'Process<>thread association',
+    description:
+      'A union of ftrace events and /proc scrapers to capture thread<>process' +
+      'associations as soon as they are seen from the cpu_pipe_raw. This is ' +
+      'to capture the information about the whole process (e.g., cmdline).',
+    supportedPlatforms: ['ANDROID', 'CHROME_OS', 'LINUX'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(...ftraceEvents);
+      const bufId = 'proc_assoc';
+      // Set to 1/16th of the main buffer size, with reasonable limits.
+      const minMax = [256, 8 * 1024];
+      const bufSizeKb = Math.min(
+        Math.max(tc.defaultBuffer.sizeKb / 16, minMax[0]),
+        minMax[1],
+      );
+      tc.addBuffer(bufId, bufSizeKb);
+
+      const ds = tc.addDataSource(PROC_STATS_DS_NAME);
+      const cfg = (ds.processStatsConfig ??= {});
+      cfg.scanAllProcessesOnStart = settings.initialScan.enabled || undefined;
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts
new file mode 100644
index 0000000..81b9d0e
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/android.ts
@@ -0,0 +1,259 @@
+// 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 {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+import {Toggle} from './widgets/toggle';
+
+export function androidRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'android',
+    title: 'Android apps & svcs',
+    subtitle: 'Android-specific data sources',
+    icon: 'android',
+    probes: [
+      atrace(),
+      logcat(),
+      frameTimeline(),
+      gameInterventions(),
+      netTracing(),
+      statsdAtoms(),
+    ],
+  };
+}
+
+function atrace(): RecordProbe {
+  const settings = {
+    categories: new TypedMultiselect<string>({
+      options: new Map(
+        Object.entries(ATRACE_CATEGORIES).map(([id, name]) => [
+          `${id}: ${name}`,
+          id,
+        ]),
+      ),
+    }),
+    allApps: new Toggle({
+      title: 'Record events from all Android apps and services',
+      cssClass: '.thin',
+    }),
+  };
+  return {
+    id: 'atrace',
+    title: 'Atrace userspace annotations',
+    image: 'rec_atrace.png',
+    description:
+      'Enables C++ / Java codebase annotations (ATRACE_BEGIN() / os.Trace())',
+    supportedPlatforms: ['ANDROID'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addAtraceCategories(...settings.categories.selectedValues());
+      if (settings.allApps.enabled) {
+        tc.addAtraceApps('*');
+      }
+      if (
+        settings.categories.selectedKeys().length > 0 ||
+        settings.allApps.enabled
+      ) {
+        tc.addFtraceEvents('ftrace/print');
+      }
+    },
+  };
+}
+
+function logcat(): RecordProbe {
+  const settings = {
+    buffers: new TypedMultiselect<protos.AndroidLogId>({
+      options: new Map(
+        Object.entries({
+          'Crash': protos.AndroidLogId.LID_CRASH,
+          'Main': protos.AndroidLogId.LID_DEFAULT,
+          'Binary events': protos.AndroidLogId.LID_EVENTS,
+          'Kernel': protos.AndroidLogId.LID_KERNEL,
+          'Radio': protos.AndroidLogId.LID_RADIO,
+          'Security': protos.AndroidLogId.LID_SECURITY,
+          'Stats': protos.AndroidLogId.LID_STATS,
+          'System': protos.AndroidLogId.LID_SYSTEM,
+        }),
+      ),
+    }),
+  };
+  return {
+    id: 'logcat',
+    title: 'Event log (logcat)',
+    image: 'rec_logcat.png',
+    description:
+      'Streams the event log into the trace. If no buffer filter is ' +
+      'specified, all buffers are selected.',
+    supportedPlatforms: ['ANDROID'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const logIds = settings.buffers.selectedValues();
+      tc.addDataSource('android.log').androidLogConfig = {
+        logIds: logIds.length > 0 ? logIds : undefined,
+      };
+    },
+  };
+}
+
+function frameTimeline(): RecordProbe {
+  return {
+    id: 'android_frame_timeline',
+    title: 'Frame timeline',
+    description:
+      'Records expected/actual frame timings from surface_flinger.' +
+      'Requires Android 12 (S) or above.',
+    supportedPlatforms: ['ANDROID'],
+    docsLink: 'https://perfetto.dev/docs/data-sources/frametimeline',
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addDataSource('android.surfaceflinger.frametimeline');
+    },
+  };
+}
+
+function gameInterventions(): RecordProbe {
+  return {
+    id: 'android_game_interventions',
+    title: 'Game intervention list',
+    description:
+      'List game modes and interventions. Requires Android 13 (T) or above.',
+    supportedPlatforms: ['ANDROID'],
+    docsLink:
+      'https://perfetto.dev/docs/data-sources/android-game-intervention-list',
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addDataSource('android.game_interventions');
+    },
+  };
+}
+
+function netTracing(): RecordProbe {
+  const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+  return {
+    id: 'network_tracing',
+    title: 'Network Tracing',
+    description:
+      'Records detailed information on network packets. ' +
+      'Requires Android 14 (U) or above',
+    supportedPlatforms: ['ANDROID'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addDataSource('android.network_packets').networkPacketTraceConfig = {
+        pollMs: settings.pollMs.value,
+      };
+    },
+  };
+}
+
+function statsdAtoms(): RecordProbe {
+  const settings = {
+    pushAtoms: new TypedMultiselect<protos.AtomId>({
+      title: 'Push atoms',
+      options: new Map(
+        Object.entries(protos.AtomId)
+          .filter(([_, v]) => typeof v === 'number' && v > 2 && v < 9999)
+          .map(([k, v]) => [k, v as protos.AtomId]),
+      ),
+    }),
+    rawPushIds: new Textarea({
+      placeholder:
+        'Add raw pushed atoms IDs, one per line, e.g.:\n' + '818\n' + '819',
+    }),
+    pullAtoms: new TypedMultiselect<protos.AtomId>({
+      title: 'Pull atoms',
+      options: new Map(
+        Object.entries(protos.AtomId)
+          .filter(([_, v]) => typeof v === 'number' && v > 10000 && v < 99999)
+          .map(([k, v]) => [k, v as protos.AtomId]),
+      ),
+    }),
+    rawPullIds: new Textarea({
+      placeholder:
+        'Add raw pulled atom IDs, one per line, e.g.:\n10063\n10064\n',
+    }),
+    pullInterval: new Slider({...POLL_INTERVAL_SLIDER, default: 5000}),
+    pullPkg: new Textarea({
+      placeholder:
+        'Add pulled atom packages, one per line, e.g.:\n' +
+        'com.android.providers.telephony',
+    }),
+  };
+  return {
+    id: 'statsd',
+    title: 'Statsd atoms',
+    description: 'Record instances of statsd atoms to the Statsd Atoms track.',
+    supportedPlatforms: ['ANDROID'],
+    docsLink:
+      'https://cs.android.com/android/platform/superproject/main/+/main:frameworks/proto_logging/stats/atoms.proto',
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const pkg = splitLinesNonEmpty(settings.pullPkg.text);
+      const pullIds = settings.pullAtoms.selectedValues();
+      const rawPullIds = splitLinesNonEmpty(settings.rawPullIds.text).map((l) =>
+        parseInt(l.trim()),
+      );
+      const hasPull = pullIds.length > 0 && rawPullIds.length > 0;
+      tc.addDataSource('android.statsd').statsdTracingConfig = {
+        pushAtomId: settings.pushAtoms.selectedValues(),
+        rawPushAtomId: splitLinesNonEmpty(settings.rawPushIds.text).map((l) =>
+          parseInt(l.trim()),
+        ),
+        pullConfig: hasPull
+          ? [
+              {
+                pullAtomId: pullIds,
+                rawPullAtomId: rawPullIds,
+                pullFrequencyMs: settings.pullInterval.value,
+                packages: pkg.length > 0 ? pkg : undefined,
+              },
+            ]
+          : undefined,
+      };
+    },
+  };
+}
+
+const ATRACE_CATEGORIES = {
+  adb: 'ADB',
+  aidl: 'AIDL calls',
+  am: 'Activity Manager',
+  audio: 'Audio',
+  binder_driver: 'Binder Kernel driver',
+  binder_lock: 'Binder global lock trace',
+  bionic: 'Bionic C library',
+  camera: 'Camera',
+  dalvik: 'ART & Dalvik',
+  database: 'Database',
+  gfx: 'Graphics',
+  hal: 'Hardware Modules',
+  input: 'Input',
+  network: 'Network',
+  nnapi: 'Neural Network API',
+  pm: 'Package Manager',
+  power: 'Power Management',
+  res: 'Resource Loading',
+  rro: 'Resource Overlay',
+  rs: 'RenderScript',
+  sm: 'Sync Manager',
+  ss: 'System Server',
+  vibrator: 'Vibrator',
+  video: 'Video',
+  view: 'View System',
+  webview: 'WebView',
+  wm: 'Window Manager',
+};
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts
new file mode 100644
index 0000000..a432725
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/buffer_config_page.ts
@@ -0,0 +1,177 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {assetSrc} from '../../../base/assets';
+import {Slider} from './widgets/slider';
+import {RecordMode, TraceConfigBuilder} from '../config/trace_config_builder';
+import {ConfigManager} from '../config/config_manager';
+import {RecordSubpage} from '../config/config_interfaces';
+import {RecordSessionSchema} from '../serialization_schema';
+import {Toggle} from './widgets/toggle';
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+
+export function bufferConfigPage(recMgr: RecordingManager): RecordSubpage {
+  return {
+    kind: 'SESSION_PAGE',
+    id: 'config',
+    icon: 'tune',
+    title: 'Buffers and duration',
+    subtitle: 'Buffer mode, size and duration',
+    render() {
+      return m(BufferConfigPage, {recMgr});
+    },
+    serialize(state: RecordSessionSchema) {
+      const tc: TraceConfigBuilder = recMgr.recordConfig.traceConfig;
+      state.mode = tc.mode;
+      state.bufSizeKb = tc.defaultBuffer.sizeKb;
+      state.durationMs = tc.durationMs;
+      state.maxFileSizeMb = tc.maxFileSizeMb;
+      state.fileWritePeriodMs = tc.fileWritePeriodMs;
+      state.compression = tc.compression;
+    },
+    async deserialize(state: RecordSessionSchema) {
+      const tc: TraceConfigBuilder = recMgr.recordConfig.traceConfig;
+      tc.mode = state.mode;
+      tc.defaultBuffer.sizeKb = state.bufSizeKb;
+      tc.durationMs = state.durationMs;
+      tc.maxFileSizeMb = state.maxFileSizeMb;
+      tc.fileWritePeriodMs = state.fileWritePeriodMs;
+      tc.compression = state.compression;
+    },
+  };
+}
+
+class BufferConfigPage implements m.ClassComponent<RecMgrAttrs> {
+  private bufSize: Slider;
+  private maxDuration: Slider;
+  private maxFileSize: Slider;
+  private flushPeriod: Slider;
+  private compress?: Toggle;
+
+  constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+    const traceCfg = attrs.recMgr.recordConfig.traceConfig;
+    this.bufSize = new Slider({
+      title: 'In-memory buffer size',
+      icon: '360',
+      values: [4, 8, 16, 32, 64, 128, 256, 512],
+      default: traceCfg.defaultBuffer.sizeKb / 1024,
+      unit: 'MB',
+      onChange: (v: number) => (traceCfg.defaultBuffer.sizeKb = v * 1024),
+    });
+    this.maxDuration = new Slider({
+      title: 'Max duration',
+      icon: 'timer',
+      values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)],
+      default: traceCfg.durationMs,
+      isTime: true,
+      unit: 'h:m:s',
+      onChange: (value: number) => (traceCfg.durationMs = value),
+    });
+    this.maxFileSize = new Slider({
+      title: 'Max file size',
+      icon: 'save',
+      values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
+      default: traceCfg.maxFileSizeMb,
+      unit: 'MB',
+      onChange: (value: number) => (traceCfg.maxFileSizeMb = value),
+    });
+    this.flushPeriod = new Slider({
+      title: 'Flush on disk every',
+      icon: 'av_timer',
+      values: [100, 250, 500, 1000, 2500, 5000],
+      default: traceCfg.fileWritePeriodMs,
+      unit: 'ms',
+      onChange: (value: number) => (traceCfg.fileWritePeriodMs = value),
+    });
+    if (!attrs.recMgr.currentTarget?.emitsCompressedtrace) {
+      this.compress = new Toggle({
+        title: 'Deflate (gzip) compression ',
+        descr:
+          'Generates smaller trace files at the cost of extra CPU cycles ' +
+          'when stopping the trace. Compression happens only after the end of ' +
+          'the trace and does not improve the ring-buffer efficiency.',
+        default: traceCfg.compression,
+        onChange: (enabled) => (traceCfg.compression = enabled),
+      });
+    }
+  }
+
+  view({attrs}: m.CVnode<RecMgrAttrs>) {
+    const recCfg = attrs.recMgr.recordConfig;
+    return [
+      m('header', 'Recording mode'),
+      m(
+        '.record-mode',
+        this.recButton(
+          recCfg,
+          'STOP_WHEN_FULL',
+          'Stop when full',
+          'rec_one_shot.png',
+        ),
+        this.recButton(
+          recCfg,
+          'RING_BUFFER',
+          'Ring buffer',
+          'rec_ring_buf.png',
+        ),
+        this.recButton(
+          recCfg,
+          'LONG_TRACE',
+          'Long trace',
+          'rec_long_trace.png',
+        ),
+      ),
+      this.bufSize.render(),
+      this.maxDuration.render(),
+      recCfg.traceConfig.mode === 'LONG_TRACE' && this.maxFileSize.render(),
+      recCfg.traceConfig.mode === 'LONG_TRACE' && this.flushPeriod.render(),
+      this.compress?.render(),
+    ];
+  }
+
+  recButton(
+    recCfg: ConfigManager,
+    mode: RecordMode,
+    title: string,
+    img: string,
+  ) {
+    const checkboxArgs = {
+      checked: recCfg.traceConfig.mode === mode,
+      onchange: (e: InputEvent) => {
+        const checked = (e.target as HTMLInputElement).checked;
+        if (!checked) return;
+        recCfg.traceConfig.mode = mode;
+        if (
+          mode === 'LONG_TRACE' &&
+          this.maxDuration.value === this.maxDuration.attrs.default
+        ) {
+          this.maxDuration.setValue(H(6));
+        }
+      },
+    };
+    return m(
+      `label${recCfg.traceConfig.mode === mode ? '.selected' : ''}`,
+      m(`input[type=radio][name=rec_mode]`, checkboxArgs),
+      m(`img[src=${assetSrc(`assets/${img}`)}]`),
+      m('span', title),
+    );
+  }
+}
+
+const S = (x: number) => x * 1000;
+const M = (x: number) => x * 1000 * 60;
+const H = (x: number) => x * 1000 * 60 * 60;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts
new file mode 100644
index 0000000..dc170b3
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/chrome.ts
@@ -0,0 +1,573 @@
+// 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 protos from '../../../protos';
+import {
+  RecordSubpage,
+  RecordProbe,
+  ProbeSetting,
+} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {Toggle} from './widgets/toggle';
+import {Section} from '../../../widgets/section';
+import {
+  MultiSelect,
+  MultiSelectDiff,
+  Option as MultiSelectOption,
+} from '../../../widgets/multiselect';
+
+type ChromeCatFunction = () => Promise<string[]>;
+
+export function chromeRecordSection(
+  chromeCategoryGetter: ChromeCatFunction,
+): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'chrome',
+    title: 'Chrome browser',
+    subtitle: 'Chrome tracing',
+    icon: 'laptop_chromebook',
+    probes: [chromeProbe(chromeCategoryGetter)],
+  };
+}
+
+function chromeProbe(chromeCategoryGetter: ChromeCatFunction): RecordProbe {
+  const groupToggles = Object.fromEntries(
+    Object.keys(GROUPS).map((groupName) => [
+      groupName,
+      new Toggle({
+        title: groupName,
+      }),
+    ]),
+  );
+  const settings = {
+    ...groupToggles,
+    privacy: new Toggle({
+      title: 'Remove untyped and sensitive data like URLs from the trace',
+      descr:
+        'Not recommended unless you intend to share the trace' +
+        ' with third-parties.',
+    }),
+    categories: new ChromeCategoriesWidget(chromeCategoryGetter),
+  };
+  return {
+    id: 'chrome_tracing',
+    title: 'Chrome browser tracing',
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const cats = new Set<string>();
+      settings.categories.getEnabledCategories().forEach((c) => cats.add(c));
+      for (const [group, groupCats] of Object.entries(GROUPS)) {
+        if ((groupToggles[group] as Toggle).enabled) {
+          groupCats.forEach((c) => cats.add(c));
+        }
+      }
+      const memoryInfra = cats.has('disabled-by-default-memory-infra');
+      const jsonStruct = {
+        record_mode:
+          tc.mode === 'STOP_WHEN_FULL'
+            ? 'record-until-full'
+            : 'record-continuously',
+        included_categories: [...cats],
+        excluded_categories: ['*'], // Only include categories explicitly
+        memory_dump_config: memoryInfra
+          ? {
+              allowed_dump_modes: ['background', 'light', 'detailed'],
+              triggers: [
+                {
+                  min_time_between_dumps_ms: 10000,
+                  mode: 'detailed',
+                  type: 'periodic_interval',
+                },
+              ],
+            }
+          : undefined,
+      };
+      const privacyFilteringEnabled = settings.privacy.enabled;
+      const chromeConfig = {
+        clientPriority: protos.ChromeConfig.ClientPriority.USER_INITIATED,
+        privacyFilteringEnabled,
+        traceConfig: JSON.stringify(jsonStruct),
+      };
+
+      const trackEvent = tc.addDataSource('track_event');
+      trackEvent.chromeConfig = chromeConfig;
+      const trackEvtCfg = (trackEvent.trackEventConfig ??= {});
+      trackEvtCfg.disabledCategories ??= ['*'];
+      trackEvtCfg.enabledCategories ??= [];
+      trackEvtCfg.enabledCategories.push(...cats);
+      trackEvtCfg.enabledCategories.push('__metadata');
+      trackEvtCfg.enableThreadTimeSampling = true;
+      trackEvtCfg.timestampUnitMultiplier = 1000;
+      trackEvtCfg.filterDynamicEventNames = privacyFilteringEnabled;
+      trackEvtCfg.filterDebugAnnotations = privacyFilteringEnabled;
+
+      tc.addDataSource('org.chromium.trace_metadata').chromeConfig =
+        chromeConfig;
+
+      if (memoryInfra) {
+        tc.addDataSource('org.chromium.memory_instrumentation').chromeConfig =
+          chromeConfig;
+        tc.addDataSource('org.chromium.native_heap_profiler').chromeConfig =
+          chromeConfig;
+      }
+
+      if (
+        cats.has('disabled-by-default-cpu_profiler') ||
+        cats.has('disabled-by-default-cpu_profiler.debug')
+      ) {
+        tc.addDataSource('org.chromium.sampler_profiler').chromeConfig =
+          chromeConfig;
+      }
+      if (cats.has('disabled-by-default-system_metrics')) {
+        tc.addDataSource('org.chromium.system_metrics').chromeConfig =
+          chromeConfig;
+      }
+    },
+  };
+}
+
+const DISAB_PREFIX = 'disabled-by-default-';
+
+export class ChromeCategoriesWidget implements ProbeSetting {
+  private options = new Array<MultiSelectOption>();
+
+  constructor(private chromeCategoryGetter: ChromeCatFunction) {
+    this.initializeCategories(BUILTIN_CATEGORIES);
+  }
+
+  private initializeCategories(cats: string[]) {
+    this.options = cats
+      .map((cat) => ({
+        id: cat,
+        name: cat.replace(DISAB_PREFIX, ''),
+        checked: false,
+      }))
+      .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+  }
+
+  getEnabledCategories(): string[] {
+    return this.options.filter((o) => o.checked).map((o) => o.id);
+  }
+
+  setEnabled(cat: string, enabled: boolean) {
+    for (const option of this.options) {
+      if (option.id !== cat) continue;
+      option.checked = enabled;
+    }
+  }
+
+  serialize() {
+    return this.options.filter((o) => o.checked).map((o) => o.id);
+  }
+
+  deserialize(state: unknown): void {
+    if (Array.isArray(state) && state.every((x) => typeof x === 'string')) {
+      this.options.forEach((o) => (o.checked = false));
+      for (const key of state) {
+        const opt = this.options.find((o) => o.id === key);
+        if (opt !== undefined) opt.checked = true;
+      }
+    }
+  }
+
+  render() {
+    return m(
+      'div.chrome-categories',
+      {
+        oninit: async () =>
+          this.initializeCategories(await this.chromeCategoryGetter()),
+      },
+      m(
+        Section,
+        {title: 'Additional Categories'},
+        m(MultiSelect, {
+          options: this.options.filter((o) => !o.id.startsWith(DISAB_PREFIX)),
+          repeatCheckedItemsAtTop: false,
+          fixedSize: false,
+          onChange: (diffs: MultiSelectDiff[]) => {
+            diffs.forEach(({id, checked}) => this.setEnabled(id, checked));
+          },
+        }),
+      ),
+      m(
+        Section,
+        {title: 'High Overhead Categories'},
+        m(MultiSelect, {
+          options: this.options.filter((o) => o.id.startsWith(DISAB_PREFIX)),
+          repeatCheckedItemsAtTop: false,
+          fixedSize: false,
+          onChange: (diffs: MultiSelectDiff[]) => {
+            diffs.forEach(({id, checked}) => this.setEnabled(id, checked));
+          },
+        }),
+      ),
+    );
+  }
+}
+
+const GROUPS = {
+  'Task Scheduling': [
+    'toplevel',
+    'toplevel.flow',
+    'scheduler',
+    'sequence_manager',
+    'disabled-by-default-toplevel.flow',
+  ],
+  'IPC Flows': [
+    'toplevel',
+    'toplevel.flow',
+    'disabled-by-default-ipc.flow',
+    'mojom',
+  ],
+  'Javascript execution': ['toplevel', 'v8'],
+  'Web content rendering, layout and compositing': [
+    'toplevel',
+    'blink',
+    'cc',
+    'gpu',
+  ],
+  'UI rendering and surface compositing': [
+    'toplevel',
+    'cc',
+    'gpu',
+    'viz',
+    'ui',
+    'views',
+  ],
+  'Input events': [
+    'toplevel',
+    'benchmark',
+    'evdev',
+    'input',
+    'disabled-by-default-toplevel.flow',
+  ],
+  'Navigation and loading': [
+    'loading',
+    'net',
+    'netlog',
+    'navigation',
+    'browser',
+  ],
+  'Audio': [
+    'base',
+    'disabled-by-default-audio',
+    'disabled-by-default-webaudio',
+    'disabled-by-default-webaudio.audionode',
+    'disabled-by-default-webrtc',
+    'disabled-by-default-audio-worklet',
+    'disabled-by-default-mediastream',
+    'disabled-by-default-v8.gc',
+    'disabled-by-default-toplevel',
+    'disabled-by-default-toplevel.flow',
+    'disabled-by-default-wakeup.flow',
+    'disabled-by-default-cpu_profiler',
+    'disabled-by-default-scheduler',
+    'disabled-by-default-p2p',
+    'disabled-by-default-net',
+  ],
+  'Video': [
+    'base',
+    'gpu',
+    'gpu.capture',
+    'media',
+    'toplevel',
+    'toplevel.flow',
+    'scheduler',
+    'wakeup.flow',
+    'webrtc',
+    'disabled-by-default-video_and_image_capture',
+    'disabled-by-default-webrtc',
+  ],
+};
+
+// List of static Chrome categories, last updated at 2024-05-15 from HEAD of
+// Chromium's //base/trace_event/builtin_categories.h.
+const BUILTIN_CATEGORIES = [
+  'accessibility',
+  'AccountFetcherService',
+  'android.adpf',
+  'android.ui.jank',
+  'android_webview',
+  'android_webview.timeline',
+  'aogh',
+  'audio',
+  'base',
+  'benchmark',
+  'blink',
+  'blink.animations',
+  'blink.bindings',
+  'blink.console',
+  'blink.net',
+  'blink.resource',
+  'blink.user_timing',
+  'blink.worker',
+  'blink_style',
+  'Blob',
+  'browser',
+  'browsing_data',
+  'CacheStorage',
+  'Calculators',
+  'CameraStream',
+  'cppgc',
+  'camera',
+  'cast_app',
+  'cast_perf_test',
+  'cast.mdns',
+  'cast.mdns.socket',
+  'cast.stream',
+  'cc',
+  'cc.debug',
+  'cdp.perf',
+  'chromeos',
+  'cma',
+  'compositor',
+  'content',
+  'content_capture',
+  'interactions',
+  'delegated_ink_trails',
+  'device',
+  'devtools',
+  'devtools.contrast',
+  'devtools.timeline',
+  'disk_cache',
+  'download',
+  'download_service',
+  'drm',
+  'drmcursor',
+  'dwrite',
+  'DXVA_Decoding',
+  'evdev',
+  'event',
+  'event_latency',
+  'exo',
+  'extensions',
+  'explore_sites',
+  'FileSystem',
+  'file_system_provider',
+  'fledge',
+  'fonts',
+  'GAMEPAD',
+  'gpu',
+  'gpu.angle',
+  'gpu.angle.texture_metrics',
+  'gpu.capture',
+  'graphics.pipeline',
+  'headless',
+  'history',
+  'hwoverlays',
+  'identity',
+  'ime',
+  'IndexedDB',
+  'input',
+  'input.scrolling',
+  'io',
+  'ipc',
+  'Java',
+  'jni',
+  'jpeg',
+  'latency',
+  'latencyInfo',
+  'leveldb',
+  'loading',
+  'log',
+  'login',
+  'media',
+  'media_router',
+  'memory',
+  'midi',
+  'mojom',
+  'mus',
+  'native',
+  'navigation',
+  'navigation.debug',
+  'net',
+  'network.scheduler',
+  'netlog',
+  'offline_pages',
+  'omnibox',
+  'oobe',
+  'openscreen',
+  'ozone',
+  'partition_alloc',
+  'passwords',
+  'p2p',
+  'page-serialization',
+  'paint_preview',
+  'pepper',
+  'PlatformMalloc',
+  'power',
+  'ppapi',
+  'ppapi_proxy',
+  'print',
+  'raf_investigation',
+  'rail',
+  'renderer',
+  'renderer_host',
+  'renderer.scheduler',
+  'resources',
+  'RLZ',
+  'ServiceWorker',
+  'SiteEngagement',
+  'safe_browsing',
+  'scheduler',
+  'scheduler.long_tasks',
+  'screenlock_monitor',
+  'segmentation_platform',
+  'sequence_manager',
+  'service_manager',
+  'sharing',
+  'shell',
+  'shortcut_viewer',
+  'shutdown',
+  'skia',
+  'sql',
+  'stadia_media',
+  'stadia_rtc',
+  'startup',
+  'sync',
+  'system_apps',
+  'test_gpu',
+  'toplevel',
+  'toplevel.flow',
+  'ui',
+  'v8',
+  'v8.execute',
+  'v8.wasm',
+  'ValueStoreFrontend::Backend',
+  'views',
+  'views.frame',
+  'viz',
+  'vk',
+  'wakeup.flow',
+  'wayland',
+  'webaudio',
+  'webengine.fidl',
+  'weblayer',
+  'WebCore',
+  'webnn',
+  'webrtc',
+  'webrtc_stats',
+  'xr',
+  'disabled-by-default-android_view_hierarchy',
+  'disabled-by-default-animation-worklet',
+  'disabled-by-default-audio',
+  'disabled-by-default-audio.latency',
+  'disabled-by-default-audio-worklet',
+  'disabled-by-default-base',
+  'disabled-by-default-blink.debug',
+  'disabled-by-default-blink.debug.display_lock',
+  'disabled-by-default-blink.debug.layout',
+  'disabled-by-default-blink.debug.layout.trees',
+  'disabled-by-default-blink.feature_usage',
+  'disabled-by-default-blink.image_decoding',
+  'disabled-by-default-blink.invalidation',
+  'disabled-by-default-identifiability',
+  'disabled-by-default-identifiability.high_entropy_api',
+  'disabled-by-default-cc',
+  'disabled-by-default-cc.debug',
+  'disabled-by-default-cc.debug.cdp-perf',
+  'disabled-by-default-cc.debug.display_items',
+  'disabled-by-default-cc.debug.lcd_text',
+  'disabled-by-default-cc.debug.picture',
+  'disabled-by-default-cc.debug.scheduler',
+  'disabled-by-default-cc.debug.scheduler.frames',
+  'disabled-by-default-cc.debug.scheduler.now',
+  'disabled-by-default-content.verbose',
+  'disabled-by-default-cpu_profiler',
+  'disabled-by-default-cppgc',
+  'disabled-by-default-cpu_profiler.debug',
+  'disabled-by-default-devtools.screenshot',
+  'disabled-by-default-devtools.timeline',
+  'disabled-by-default-devtools.timeline.frame',
+  'disabled-by-default-devtools.timeline.inputs',
+  'disabled-by-default-devtools.timeline.invalidationTracking',
+  'disabled-by-default-devtools.timeline.layers',
+  'disabled-by-default-devtools.timeline.picture',
+  'disabled-by-default-devtools.timeline.stack',
+  'disabled-by-default-devtools.target-rundown',
+  'disabled-by-default-devtools.v8-source-rundown',
+  'disabled-by-default-devtools.v8-source-rundown-sources',
+  'disabled-by-default-file',
+  'disabled-by-default-fonts',
+  'disabled-by-default-gpu_cmd_queue',
+  'disabled-by-default-gpu.dawn',
+  'disabled-by-default-gpu.debug',
+  'disabled-by-default-gpu.decoder',
+  'disabled-by-default-gpu.device',
+  'disabled-by-default-gpu.graphite.dawn',
+  'disabled-by-default-gpu.service',
+  'disabled-by-default-gpu.vulkan.vma',
+  'disabled-by-default-histogram_samples',
+  'disabled-by-default-java-heap-profiler',
+  'disabled-by-default-layer-element',
+  'disabled-by-default-layout_shift.debug',
+  'disabled-by-default-lifecycles',
+  'disabled-by-default-loading',
+  'disabled-by-default-mediastream',
+  'disabled-by-default-memory-infra',
+  'disabled-by-default-memory-infra.v8.code_stats',
+  'disabled-by-default-mojom',
+  'disabled-by-default-net',
+  'disabled-by-default-network',
+  'disabled-by-default-paint-worklet',
+  'disabled-by-default-power',
+  'disabled-by-default-renderer.scheduler',
+  'disabled-by-default-renderer.scheduler.debug',
+  'disabled-by-default-sequence_manager',
+  'disabled-by-default-sequence_manager.debug',
+  'disabled-by-default-sequence_manager.verbose_snapshots',
+  'disabled-by-default-skia',
+  'disabled-by-default-skia.gpu',
+  'disabled-by-default-skia.gpu.cache',
+  'disabled-by-default-skia.shaders',
+  'disabled-by-default-skottie',
+  'disabled-by-default-SyncFileSystem',
+  'disabled-by-default-system_power',
+  'disabled-by-default-system_stats',
+  'disabled-by-default-thread_pool_diagnostics',
+  'disabled-by-default-toplevel.ipc',
+  'disabled-by-default-user_action_samples',
+  'disabled-by-default-v8.compile',
+  'disabled-by-default-v8.cpu_profiler',
+  'disabled-by-default-v8.gc',
+  'disabled-by-default-v8.gc_stats',
+  'disabled-by-default-v8.ic_stats',
+  'disabled-by-default-v8.inspector',
+  'disabled-by-default-v8.runtime',
+  'disabled-by-default-v8.runtime_stats',
+  'disabled-by-default-v8.runtime_stats_sampling',
+  'disabled-by-default-v8.stack_trace',
+  'disabled-by-default-v8.turbofan',
+  'disabled-by-default-v8.wasm.detailed',
+  'disabled-by-default-v8.wasm.turbofan',
+  'disabled-by-default-video_and_image_capture',
+  'disabled-by-default-display.framedisplayed',
+  'disabled-by-default-viz.gpu_composite_time',
+  'disabled-by-default-viz.debug.overlay_planes',
+  'disabled-by-default-viz.hit_testing_flow',
+  'disabled-by-default-viz.overdraw',
+  'disabled-by-default-viz.quads',
+  'disabled-by-default-viz.surface_id_flow',
+  'disabled-by-default-viz.surface_lifetime',
+  'disabled-by-default-viz.triangles',
+  'disabled-by-default-viz.visual_debugger',
+  'disabled-by-default-webaudio.audionode',
+  'disabled-by-default-webgpu',
+  'disabled-by-default-webnn',
+  'disabled-by-default-webrtc',
+  'disabled-by-default-worker.scheduler',
+  'disabled-by-default-xr.debug',
+];
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts
new file mode 100644
index 0000000..eac9429
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/cpu.ts
@@ -0,0 +1,123 @@
+// 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 protos from '../../../protos';
+import {ADV_FTRACE_PROBE_ID, ADV_PROC_ASSOC_PROBE_ID} from './advanced';
+import {RecordSubpage, RecordProbe} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+
+const PROC_POLL_DS = 'linux.sys_stats';
+
+export function cpuRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'cpu',
+    title: 'CPU',
+    subtitle: 'CPU usage, scheduling, wakeups',
+    icon: 'subtitles',
+    probes: [cpuUsage(), sched(), cpuFreq(), syscalls()],
+  };
+}
+
+function cpuUsage(): RecordProbe {
+  const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+  return {
+    id: 'cpu_usage',
+    image: 'rec_cpu_coarse.png',
+    title: 'Coarse CPU usage counter',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    description:
+      'Lightweight polling of CPU usage counters via /proc/stat. ' +
+      'Allows to periodically monitor CPU usage.',
+    dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const cfg = tc.addDataSource(PROC_POLL_DS);
+      cfg.sysStatsConfig ??= {};
+      cfg.sysStatsConfig.statPeriodMs = settings.pollMs.value;
+      cfg.sysStatsConfig.statCounters ??= [];
+      cfg.sysStatsConfig.statCounters.push(
+        protos.SysStatsConfig.StatCounters.STAT_CPU_TIMES,
+        protos.SysStatsConfig.StatCounters.STAT_FORK_COUNT,
+      );
+    },
+  };
+}
+
+function sched(): RecordProbe {
+  return {
+    id: 'cpu_sched',
+    image: 'rec_cpu_fine.png',
+    title: 'Scheduling details',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    dependencies: [ADV_FTRACE_PROBE_ID, ADV_PROC_ASSOC_PROBE_ID],
+    description: 'Enables high-detailed tracking of scheduling events',
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(
+        'sched/sched_switch',
+        'power/suspend_resume',
+        'sched/sched_blocked_reason',
+        'sched/sched_wakeup',
+        'sched/sched_wakeup_new',
+        'sched/sched_waking',
+        'sched/sched_process_exit',
+        'sched/sched_process_free',
+        'task/task_newtask',
+        'task/task_rename',
+      );
+    },
+  };
+}
+
+function cpuFreq(): RecordProbe {
+  const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+  return {
+    id: 'cpu_freq',
+    image: 'rec_cpu_freq.png',
+    title: 'CPU frequency and idle states',
+    description:
+      'Records cpu frequency and idle state changes via ftrace and sysfs',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const cfg = tc.addDataSource(PROC_POLL_DS);
+      cfg.sysStatsConfig ??= {};
+      cfg.sysStatsConfig.cpufreqPeriodMs = settings.pollMs.value;
+      tc.addFtraceEvents(
+        'power/cpu_frequency',
+        'power/cpu_idle',
+        'power/suspend_resume',
+      );
+    },
+  };
+}
+
+function syscalls(): RecordProbe {
+  return {
+    id: 'cpu_syscalls',
+    image: 'rec_syscalls.png',
+    title: 'Syscalls',
+    description:
+      'Tracks the enter and exit of all syscalls. On Android' +
+      'requires a userdebug or eng build.',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(
+        'raw_syscalls/sys_enter', //
+        'raw_syscalls/raw_exit', //
+      );
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts
new file mode 100644
index 0000000..aee581b
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/gpu.ts
@@ -0,0 +1,70 @@
+// 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 {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+
+export function gpuRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'gpu',
+    title: 'GPU',
+    subtitle: 'GPU Frequency, memory',
+    icon: 'aspect_ratio',
+    probes: [gpuFreq(), gpuMemory(), gpuWorkPeriod()],
+  };
+}
+
+function gpuFreq(): RecordProbe {
+  return {
+    id: 'gpu_frequency',
+    image: 'rec_cpu_freq.png',
+    title: 'GPU frequency',
+    description: 'Records gpu frequency via ftrace',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents('power/gpu_frequency');
+    },
+  };
+}
+
+function gpuMemory(): RecordProbe {
+  return {
+    id: 'gpu_memory',
+    image: 'rec_gpu_mem_total.png',
+    title: 'GPU memory',
+    description:
+      'Allows to track per process and global total GPU memory usages. ' +
+      '(Available on recent Android 12+ kernels)',
+    supportedPlatforms: ['ANDROID'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addDataSource('android.gpu.memory');
+      tc.addFtraceEvents('gpu_mem/gpu_mem_total');
+    },
+  };
+}
+
+function gpuWorkPeriod(): RecordProbe {
+  return {
+    id: 'gpu_work_period',
+    title: 'GPU work period',
+    description:
+      'Allows to track per package GPU work.' +
+      '(Available on recent Android 14+ kernels)',
+    supportedPlatforms: ['ANDROID'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents('power/gpu_work_period');
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts
new file mode 100644
index 0000000..ebe879f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/instructions_page.ts
@@ -0,0 +1,102 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {copyToClipboard} from '../../../base/clipboard';
+import {traceConfigToTxt} from '../config/trace_config_utils_wasm';
+import protos from '../../../protos';
+import {RecordSubpage} from '../config/config_interfaces';
+import {Anchor} from '../../../widgets/anchor';
+
+export function instructionsPage(recMgr: RecordingManager): RecordSubpage {
+  return {
+    kind: 'GLOBAL_PAGE',
+    id: 'cmdline',
+    icon: 'terminal',
+    title: 'Cmdline instructions',
+    subtitle: 'Show cmdline instructions',
+    render() {
+      return m(InstructionsPage, {recMgr});
+    },
+    serialize() {},
+    deserialize() {},
+  };
+}
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+class InstructionsPage implements m.ClassComponent<RecMgrAttrs> {
+  private configTxt = '';
+  private cmdline?: string;
+  private docsLink?: string;
+
+  constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+    // Generate the config PBTX.
+    const cfg = attrs.recMgr.genTraceConfig();
+    const cfgBytes = protos.TraceConfig.encode(cfg).finish().slice();
+    traceConfigToTxt(cfgBytes).then((txt) => {
+      this.configTxt = txt;
+      m.redraw();
+    });
+
+    // Generate the cmdline instructions.
+    switch (attrs.recMgr.currentPlatform) {
+      case 'ANDROID':
+        this.cmdline =
+          'cat config.pbtx | adb shell perfetto' +
+          ' -c - --txt -o /data/misc/perfetto-traces/trace.pftrace';
+        this.docsLink = 'https://perfetto.dev/docs/quickstart/android-tracing';
+        break;
+      case 'LINUX':
+        this.cmdline = 'perfetto -c config.pbtx --txt -o /tmp/trace.pftrace';
+        this.docsLink = 'https://perfetto.dev/docs/quickstart/linux-tracing';
+        break;
+      case 'CHROME':
+      case 'CHROME_OS':
+        this.docsLink = 'https://perfetto.dev/docs/quickstart/chrome-tracing';
+        this.cmdline =
+          'There is no cmdline support for Chrome/CrOS.\n' +
+          'You must use the recording UI via the extension to record traces.';
+    }
+  }
+
+  view() {
+    return [
+      this.docsLink &&
+        m(
+          'p',
+          'See the documentation on ',
+          m(
+            Anchor,
+            {href: this.docsLink, target: '_blank'},
+            this.docsLink.replace('https://', ''),
+          ),
+        ),
+      this.cmdline && m('.code-snippet', m('code', this.cmdline)),
+      m('p', 'Save the file below as: config.pbtx'),
+      m(
+        '.code-snippet',
+        m(
+          'button',
+          {
+            title: 'Copy to clipboard',
+            onclick: () => copyToClipboard(this.configTxt),
+          },
+          m('i.material-icons', 'assignment'),
+        ),
+        m('code', this.configTxt),
+      ),
+    ];
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts
new file mode 100644
index 0000000..fe7a6fb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/memory.ts
@@ -0,0 +1,384 @@
+// 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 {assertExists} from '../../../base/logging';
+import {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {ADV_PROC_ASSOC_PROBE_ID, PROC_STATS_DS_NAME} from './advanced';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {TypedMultiselect} from './widgets/multiselect';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+import {Toggle} from './widgets/toggle';
+
+const SYS_STAT_DS = 'linux.sys_stats';
+
+export function memoryRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'memory',
+    title: 'Memory',
+    subtitle: 'Physical mem, VM, LMK',
+    icon: 'memory',
+    probes: [
+      heapProfiling(),
+      heapDumps(),
+      meminfo(),
+      vmstat(),
+      hifreq(),
+      lmk(),
+      polledProcStats(),
+    ],
+  };
+}
+
+function heapProfiling(): RecordProbe {
+  const settings = {
+    targetProcs: new Textarea({
+      title: 'Names or pids of the processes to track (required)',
+      docsLink:
+        'https://perfetto.dev/docs/data-sources/native-heap-profiler#heapprofd-targets',
+      placeholder:
+        'One per line, e.g.:\n' +
+        'system_server\n' +
+        'com.google.android.apps.photos\n' +
+        '1503',
+    }),
+    samplingBytes: new Slider({
+      title: 'Sampling interval',
+      description: 'Trades off accuracy vs overhead in the target process',
+      cssClass: '.thin',
+      default: 4096,
+      values: [
+        1, 16, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536,
+        131072, 262144, 524288, 1048576,
+      ],
+      unit: 'B',
+      min: 1,
+    }),
+    dumpInterval: new Slider({
+      title: 'Continuous dump interval',
+      description: 'Time between following dumps (0 = only dump at the end)',
+      values: SAMPLING_TIMES_MS,
+      cssClass: '.thin',
+      unit: 'ms',
+      min: 0,
+    }),
+    dumpPhase: new Slider({
+      title: 'Continuous dumps phase',
+      description: 'Time before first dump',
+      values: SAMPLING_TIMES_MS,
+      cssClass: '.thin',
+      unit: 'ms',
+      min: 0,
+    }),
+    shmemKB: new Slider({
+      title: 'Shared memory buffer',
+      values: SMB_VALUES_KB,
+      cssClass: '.thin',
+      unit: 'KB',
+    }),
+    blockClient: new Toggle({
+      title: 'Block client',
+      cssClass: '.thin',
+      default: true,
+      descr: `Slow down target application if profiler cannot keep up.`,
+    }),
+    allHeaps: new Toggle({
+      title: 'All custom allocators (Q+)',
+      cssClass: '.thin',
+      descr:
+        'If the target application exposes custom allocators, also ' +
+        'sample from those.',
+    }),
+  };
+  return {
+    id: 'mem_hprof',
+    title: 'Native heap profiling',
+    image: 'rec_native_heap_profiler.png',
+    description:
+      'Track native heap allocations & deallocations of an Android ' +
+      'process. (Available on Android 10+)',
+    supportedPlatforms: ['ANDROID', 'LINUX'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const s = settings;
+      const [cmdlines, pids] = extractCmdlinesAndPids(s.targetProcs.text);
+      tc.addDataSource('android.heapprofd').heapprofdConfig = {
+        samplingIntervalBytes: s.samplingBytes.value,
+        shmemSizeBytes: s.shmemKB.value * 1024,
+        blockClient: s.blockClient.enabled,
+        allHeaps: s.allHeaps.enabled,
+        processCmdline: cmdlines.length > 0 ? cmdlines : undefined,
+        pid: pids.length > 0 ? pids : undefined,
+        continuousDumpConfig:
+          s.dumpInterval.value == 0
+            ? undefined
+            : {
+                dumpIntervalMs: s.dumpInterval.value,
+                dumpPhaseMs: s.dumpPhase.value,
+              },
+      };
+    },
+  };
+}
+
+function heapDumps(): RecordProbe {
+  const settings = {
+    targetProcs: new Textarea({
+      title: 'Names or pids of the processes to track (required)',
+      docsLink: 'https://perfetto.dev/docs/data-sources/java-heap-profiler',
+      placeholder:
+        'One per line, e.g.:\n' +
+        'system_server\n' +
+        'com.google.android.apps.photos\n' +
+        '1503',
+    }),
+    dumpInterval: new Slider({
+      title: 'Continuous dump interval',
+      description: 'Time between following dumps (0 = only dump at the end)',
+      values: SAMPLING_TIMES_MS,
+      cssClass: '.thin',
+      unit: 'ms',
+      min: 0,
+    }),
+    dumpPhase: new Slider({
+      title: 'Continuous dumps phase',
+      description: 'Time before first dump',
+      values: SAMPLING_TIMES_MS,
+      cssClass: '.thin',
+      unit: 'ms',
+      min: 0,
+    }),
+  };
+  return {
+    id: 'mem_heapdumps',
+    title: 'Java heap dumps',
+    image: 'rec_java_heap_dump.png',
+    description:
+      'Dump information about the Java object graph of an ' +
+      'Android app. (Available on Android 11+)',
+    supportedPlatforms: ['ANDROID'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const s = settings;
+      const [cmdlines, pids] = extractCmdlinesAndPids(s.targetProcs.text);
+      tc.addDataSource('android.java_hprof').javaHprofConfig = {
+        processCmdline: cmdlines.length > 0 ? cmdlines : undefined,
+        pid: pids.length > 0 ? pids : undefined,
+        continuousDumpConfig:
+          s.dumpInterval.value == 0
+            ? undefined
+            : {
+                dumpIntervalMs: s.dumpInterval.value,
+                dumpPhaseMs: s.dumpPhase.value,
+              },
+      };
+    },
+  };
+}
+
+function meminfo(): RecordProbe {
+  const meminfoCounters = new Map<string, protos.MeminfoCounters>();
+  for (const x in protos.MeminfoCounters) {
+    if (
+      typeof protos.MeminfoCounters[x] === 'number' &&
+      !`${x}`.endsWith('_UNSPECIFIED')
+    ) {
+      meminfoCounters.set(
+        x.replace('MEMINFO_', '').toLowerCase(),
+        protos.MeminfoCounters[x],
+      );
+    }
+  }
+  const settings = {
+    pollMs: new Slider(POLL_INTERVAL_SLIDER),
+    counters: new TypedMultiselect<protos.MeminfoCounters>({
+      options: meminfoCounters,
+    }),
+  };
+  return {
+    id: 'mem_meminfo',
+    image: 'rec_meminfo.png',
+    title: 'Kernel meminfo',
+    description: 'Polling of /proc/meminfo',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const ds = tc.addDataSource(SYS_STAT_DS);
+      // sysStatsConfig is shared with other probes, don't clobber.
+      const cfg = (ds.sysStatsConfig ??= {});
+      cfg.meminfoPeriodMs = settings.pollMs.value;
+      cfg.meminfoCounters = settings.counters.selectedValues();
+    },
+  };
+}
+
+function vmstat(): RecordProbe {
+  const vmstatCounters = new Map<string, protos.VmstatCounters>();
+  for (const x in protos.VmstatCounters) {
+    if (
+      typeof protos.VmstatCounters[x] === 'number' &&
+      !`${x}`.endsWith('_UNSPECIFIED')
+    ) {
+      vmstatCounters.set(
+        x.replace('VMSTAT_', '').toLowerCase(),
+        protos.VmstatCounters[x],
+      );
+    }
+  }
+  const settings = {
+    pollMs: new Slider(POLL_INTERVAL_SLIDER),
+    counters: new TypedMultiselect<protos.VmstatCounters>({
+      options: vmstatCounters,
+    }),
+  };
+  return {
+    id: 'mem_vmstat',
+    title: 'Virtual memory stats',
+    image: 'rec_vmstat.png',
+    description:
+      'Periodically polls virtual memory stats from /proc/vmstat. ' +
+      'Allows to gather statistics about swap, eviction, ' +
+      'compression and pagecache efficiency',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const ds = tc.addDataSource(SYS_STAT_DS);
+      // sysStatsConfig is shared with other probes, don't clobber.
+      const cfg = (ds.sysStatsConfig ??= {});
+      cfg.vmstatPeriodMs = settings.pollMs.value;
+      cfg.vmstatCounters = settings.counters.selectedValues();
+    },
+  };
+}
+
+function hifreq(): RecordProbe {
+  return {
+    id: 'mem_hifreq',
+    title: 'High-frequency memory events',
+    image: 'rec_mem_hifreq.png',
+    dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+    description:
+      'Allows to track short memory spikes and transitories through ' +
+      "ftrace's mm_event, rss_stat and ion events. Available only " +
+      'on recent Android Q+ kernels',
+    supportedPlatforms: ['ANDROID'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(
+        'mm_event/mm_event_record',
+        'kmem/rss_stat',
+        'ion/ion_stat',
+        'dmabuf_heap/dma_heap_stat',
+        'kmem/ion_heap_grow',
+        'kmem/ion_heap_shrink',
+      );
+    },
+  };
+}
+
+function lmk(): RecordProbe {
+  return {
+    id: 'mem_lmk',
+    title: 'Low memory killer',
+    image: 'rec_lmk.png',
+    dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+    description:
+      'Record LMK events. Works both with the old in-kernel LMK ' +
+      'and the newer userspace lmkd. It also tracks OOM score adjustments.',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(
+        // For in-kernel LMK (roughly older devices until Go and Pixel 3).
+        'lowmemorykiller/lowmemory_kill',
+        'oom/oom_score_adj_update',
+      );
+
+      // For userspace LMKd (newer devices).
+      // 'lmkd' is not really required because the code in lmkd.c emits events
+      // with ATRACE_TAG_ALWAYS. We need something just to ensure that the final
+      // config will enable atrace userspace events.
+      tc.addAtraceApps('lmkd');
+    },
+  };
+}
+
+function polledProcStats(): RecordProbe {
+  const settings = {
+    pollMs: new Slider(POLL_INTERVAL_SLIDER),
+    procAge: new Toggle({title: 'Record process age'}),
+    procRuntime: new Toggle({title: 'Record process runtime'}),
+  };
+  return {
+    id: 'mem_proc_stat',
+    title: 'Per process /proc/ stat polling',
+    image: 'rec_ps_stats.png',
+    dependencies: [ADV_PROC_ASSOC_PROBE_ID],
+    description:
+      'Periodically samples all processes in the system tracking: ' +
+      'their thread list, memory counters (RSS, swap and other ' +
+      '/proc/status counters) and oom_score_adj.',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const ds = tc.addDataSource(PROC_STATS_DS_NAME);
+      // Because of the dependency on ADV_PROC_ASSOC_PROBE_ID, we expect
+      // procThreadAssociation() to create the config first.
+      const cfg = assertExists(ds.processStatsConfig);
+      cfg.procStatsPollMs = settings.pollMs.value || undefined;
+      cfg.recordProcessAge = settings.procAge.enabled || undefined;
+      cfg.recordProcessRuntime = settings.procRuntime.enabled || undefined;
+    },
+  };
+}
+
+const SAMPLING_TIMES_MS = [
+  0,
+  1000,
+  10 * 1000,
+  30 * 1000,
+  60 * 1000,
+  5 * 60 * 1000,
+  10 * 60 * 1000,
+  30 * 60 * 1000,
+  60 * 60 * 1000,
+];
+
+const SMB_VALUES_KB = [
+  16,
+  32,
+  64,
+  128,
+  512,
+  1024,
+  4096,
+  16 * 1024,
+  32 * 1024,
+  128 * 1024,
+];
+
+function extractCmdlinesAndPids(text: string): [string[], number[]] {
+  const cmdlines = [];
+  const pids = [];
+  for (const line of splitLinesNonEmpty(text)) {
+    const num = parseInt(line);
+    if (isNaN(num)) {
+      cmdlines.push(line);
+    } else {
+      pids.push(num);
+    }
+  }
+  return [cmdlines, pids];
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.ts
new file mode 100644
index 0000000..ee3d232
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/power.ts
@@ -0,0 +1,78 @@
+// 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 protos from '../../../protos';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {POLL_INTERVAL_SLIDER, Slider} from './widgets/slider';
+
+export function powerRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'power',
+    title: 'Power',
+    subtitle: 'Battery and other energy counters',
+    icon: 'battery_charging_full',
+    probes: [powerRails(), powerVoltages()],
+  };
+}
+
+function powerRails(): RecordProbe {
+  const ANDROID_POWER_DS = 'android.power';
+  const settings = {pollMs: new Slider(POLL_INTERVAL_SLIDER)};
+  return {
+    id: 'power_rails',
+    image: 'rec_battery_counters.png',
+    title: 'Battery drain & power rails',
+    description:
+      'Polls charge counters and instantaneous power draw from ' +
+      'the battery power management IC and the power rails from ' +
+      'the PowerStats HAL.',
+    docsLink: 'https://perfetto.dev/docs/data-sources/battery-counters',
+    supportedPlatforms: ['ANDROID'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addDataSource(ANDROID_POWER_DS).androidPowerConfig = {
+        batteryPollMs: settings.pollMs.value,
+        collectPowerRails: true,
+        batteryCounters: [
+          protos.AndroidPowerConfig.BatteryCounters
+            .BATTERY_COUNTER_CAPACITY_PERCENT,
+          protos.AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
+          protos.AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
+        ],
+      };
+    },
+  };
+}
+
+function powerVoltages(): RecordProbe {
+  return {
+    id: 'power_voltages',
+    image: 'rec_board_voltage.png',
+    title: 'Board voltages & frequencies',
+    description: 'Tracks voltage and frequency changes from board sensors',
+    supportedPlatforms: ['ANDROID', 'LINUX', 'CHROME_OS'],
+    genConfig: function (tc: TraceConfigBuilder) {
+      tc.addFtraceEvents(
+        'regulator/regulator_set_voltage',
+        'regulator/regulator_set_voltage_complete',
+        'power/clock_enable',
+        'power/clock_disable',
+        'power/clock_set_rate',
+        'power/suspend_resume',
+      );
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts
new file mode 100644
index 0000000..289c1a6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/preflight_check_renderer.ts
@@ -0,0 +1,82 @@
+// 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 {
+  PreflightCheck,
+  PreflightCheckResult,
+  WithPreflightChecks,
+} from '../interfaces/connection_check';
+import {Spinner} from '../../../widgets/spinner';
+import {Icon} from '../../../widgets/icon';
+
+type PreflightCheckWithResult = PreflightCheck & {
+  result?: PreflightCheckResult;
+};
+
+export class PreflightCheckRenderer {
+  private results = new Array<PreflightCheckWithResult>();
+  private allChecksCompleted = false;
+  private numChecksFailed = 0;
+
+  constructor(private testTarget: WithPreflightChecks) {}
+
+  async runPreflightChecks(): Promise<boolean> {
+    this.allChecksCompleted = false;
+    this.numChecksFailed = 0;
+    for await (const check of this.testTarget.runPreflightChecks()) {
+      const entry: PreflightCheckWithResult = {...check, result: check.status};
+      this.results.push(entry);
+      this.numChecksFailed += check.status.ok ? 0 : 1;
+      m.redraw();
+    }
+    this.allChecksCompleted = true;
+    m.redraw();
+    return this.numChecksFailed === 0;
+  }
+
+  renderIcon(): m.Children {
+    const attrs = {filled: true, className: 'preflight-checks-icon'};
+    if (!this.allChecksCompleted) {
+      return m(Spinner);
+    }
+    if (this.numChecksFailed > 0) {
+      attrs.className += ' ok';
+      return m(Icon, {icon: 'report', ...attrs});
+    }
+    attrs.className += ' error';
+    return m(Icon, {icon: 'check_circle', ...attrs});
+  }
+
+  renderTable(): m.Children {
+    return m(
+      'table.preflight-checks-table',
+      this.results.map((res) =>
+        m(
+          'tr',
+          m('td', res.name),
+          m(
+            'td',
+            res.result === undefined
+              ? m(Spinner)
+              : res.result.ok
+                ? m('span.ok', res.result.value)
+                : m('span.error', res.result.error),
+            res.remediation && m('div', m(res.remediation)),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts
new file mode 100644
index 0000000..1089c3f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/probe_renderer.ts
@@ -0,0 +1,86 @@
+// 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 {assetSrc} from '../../../base/assets';
+import {ConfigManager} from '../config/config_manager';
+import {RecordProbe} from '../config/config_interfaces';
+import {exists} from '../../../base/utils';
+import {DocsChip} from './widgets/docs_chip';
+import {classNames} from '../../../base/classnames';
+
+export interface ProbeAttrs {
+  cfgMgr: ConfigManager;
+  probe: RecordProbe;
+}
+
+export class Probe implements m.ClassComponent<ProbeAttrs> {
+  view({attrs}: m.CVnode<ProbeAttrs>) {
+    const onToggle = (enabled: boolean) => {
+      attrs.cfgMgr.setProbeEnabled(attrs.probe.id, enabled);
+    };
+
+    const probe = attrs.probe;
+    const forceEnabledDeps = attrs.cfgMgr.getProbeEnableDependants(
+      attrs.probe.id,
+    );
+    const enabled = attrs.cfgMgr.isProbeEnabled(attrs.probe.id);
+    const compact =
+      !exists(probe.description) &&
+      !exists(probe.image) &&
+      (probe.settings ?? []).length === 0;
+    return m(
+      '.probe',
+      {
+        className: classNames(enabled && 'enabled', compact && 'compact'),
+      },
+      probe.image &&
+        m('img', {
+          src: assetSrc(`assets/${probe.image}`),
+          onclick: () => onToggle(!enabled),
+        }),
+      m(
+        'label',
+        m(`input[type=checkbox]`, {
+          checked: enabled,
+          disabled: forceEnabledDeps.length > 0,
+          title:
+            forceEnabledDeps.length > 0
+              ? 'Force-enabled due to ' + forceEnabledDeps.join(',')
+              : '',
+          oninput: (e: InputEvent) => {
+            onToggle((e.target as HTMLInputElement).checked);
+          },
+        }),
+        m('span', probe.title),
+      ),
+      compact
+        ? ''
+        : m(
+            `div${probe.image ? '' : '.extended-desc'}`,
+            m(
+              'div',
+              probe.description,
+              probe.docsLink && m(DocsChip, {href: probe.docsLink}),
+            ),
+            m(
+              '.probe-config',
+              Object.values(attrs.probe.settings ?? {}).map((widget) =>
+                widget.render(),
+              ),
+            ),
+          ),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts
new file mode 100644
index 0000000..7f44beb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/record_page.ts
@@ -0,0 +1,282 @@
+// 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 {PageAttrs} from '../../../public/page';
+import {RecordingManager} from '../recording_manager';
+import {Icon} from '../../../widgets/icon';
+import {RecordSubpage, supportsPlatform} from '../config/config_interfaces';
+import {Probe} from './probe_renderer';
+import {Button} from '../../../widgets/button';
+import {classNames} from '../../../base/classnames';
+import {showModal} from '../../../widgets/modal';
+import {CopyableLink} from '../../../widgets/copyable_link';
+import {assertExists} from '../../../base/logging';
+import {BUCKET_NAME} from '../../../base/gcs_uploader';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {exists} from '../../../base/utils';
+
+export type RecordPageAttrs = PageAttrs & {
+  getRecordingManager: () => RecordingManager;
+};
+
+const DEFAULT_SUBPAGE = 'target';
+const PERSIST_EVERY_MS = 1000;
+const SHARE_SUBPAGE = 'share';
+
+// By design this interface overlaps with RecordConfigSection so we can use the
+// same for custom subpages (record, config) and the probe settings.
+interface MenuEntry {
+  readonly id: string;
+  readonly icon: string;
+  readonly title: string;
+  readonly subtitle: string;
+}
+
+export class RecordPageV2 implements m.ClassComponent<RecordPageAttrs> {
+  private recMgr: RecordingManager;
+  private subpage: string = DEFAULT_SUBPAGE;
+  private persistTimer: number | undefined = undefined;
+
+  constructor({attrs}: m.CVnode<RecordPageAttrs>) {
+    this.recMgr = attrs.getRecordingManager();
+    if (attrs.subpage && attrs.subpage.startsWith('/' + SHARE_SUBPAGE)) {
+      this.loadShared(attrs.subpage.substring(SHARE_SUBPAGE.length + 2));
+    }
+  }
+
+  view({attrs}: m.CVnode<RecordPageAttrs>) {
+    if (this.persistTimer === undefined) {
+      this.persistTimer = window.setTimeout(() => {
+        this.recMgr.persistIntoLocalStorage();
+        this.persistTimer = undefined;
+      }, PERSIST_EVERY_MS);
+    }
+    this.subpage =
+      exists(attrs.subpage) && attrs.subpage.length > 0
+        ? attrs.subpage.substring(1)
+        : DEFAULT_SUBPAGE;
+    return m(
+      '.record-page',
+      m(
+        '.record-container',
+        m(
+          '.record-container-content',
+          this.renderMenu(), //
+          this.renderSubPage(), //
+        ),
+      ),
+    );
+  }
+
+  onremove() {
+    window.clearTimeout(this.persistTimer);
+    this.recMgr.persistIntoLocalStorage();
+  }
+
+  private renderSubPage(): m.Children {
+    const page = this.recMgr.pages.get(this.subpage);
+    if (page === undefined) {
+      return m(
+        '.record-section.active',
+        m('header', `Invalid subpage /record/${this.subpage}`),
+      );
+    }
+    return [
+      m(
+        '.record-section.active',
+        {id: page.id, key: page.id},
+        this.renderSubpage(page),
+      ),
+    ];
+  }
+
+  private renderSubpage(page: RecordSubpage): m.Children {
+    switch (page.kind) {
+      case 'PROBES_PAGE':
+        return page.probes
+          .filter((p) => supportsPlatform(p, this.recMgr.currentPlatform))
+          .map((probe) => m(Probe, {cfgMgr: this.recMgr.recordConfig, probe}));
+      case 'GLOBAL_PAGE':
+      case 'SESSION_PAGE':
+        return page.render();
+    }
+  }
+
+  private renderMenu() {
+    const pages = Array.from(this.recMgr.pages.values());
+    return m(
+      '.record-menu',
+      m(RecordingCtl, {recMgr: this.recMgr}),
+      m(
+        'header',
+        'Record settings',
+        m(Button, {
+          icon: 'share',
+          title: 'Share current config',
+          onclick: () => this.share(),
+        }),
+      ),
+      m(
+        'ul',
+        pages
+          .filter((p) => ['SESSION_PAGE', 'GLOBAL_PAGE'].includes(p.kind))
+          .map((rc) => this.renderMenuEntry(rc)),
+      ),
+      m(
+        'header',
+        'Probes',
+        m(Button, {
+          icon: 'delete_sweep',
+          title: 'Clear current configuration',
+          onclick: () => {
+            if (confirm('The current config will be cleared. Are you sure?')) {
+              this.recMgr.clearSession();
+            }
+          },
+        }),
+      ),
+      m(
+        'ul',
+        pages
+          .filter((p) => p.kind === 'PROBES_PAGE')
+          .map((rc) => this.renderMenuEntry(rc)),
+      ),
+    );
+  }
+
+  private renderMenuEntry(rc: MenuEntry) {
+    let enabledProbes = 0;
+    let availProbes = 0;
+    let probeCountTxt = '';
+    const probePage = this.recMgr.pages.get(rc.id);
+    if (probePage?.kind === 'PROBES_PAGE') {
+      for (const probe of probePage.probes) {
+        if (!supportsPlatform(probe, this.recMgr.currentPlatform)) continue;
+        ++availProbes;
+        if (!this.recMgr.recordConfig.isProbeEnabled(probe.id)) continue;
+        ++enabledProbes;
+      }
+      probeCountTxt = `${enabledProbes > 0 ? enabledProbes : ''}`;
+    }
+    const disabled = availProbes === 0 && probePage?.kind === 'PROBES_PAGE';
+    const className = classNames(
+      this.subpage === rc.id && 'active',
+      disabled && 'disabled',
+    );
+    return m(
+      'a',
+      {href: disabled ? undefined : `#!/record/${rc.id}`},
+      m(
+        'li',
+        {className},
+        m(Icon, {icon: rc.icon}),
+        m('.title', rc.title, m('.probe-count', probeCountTxt)),
+        m('.sub', rc.subtitle),
+      ),
+    );
+  }
+
+  private async share() {
+    const msg =
+      'This will generate a publicly-readable link to the ' +
+      'current config which cannot be deleted. Continue?';
+    if (!confirm(msg)) return;
+    const url = await this.recMgr.share();
+    const hash = assertExists(url.split('/').pop());
+    showModal({
+      title: 'Permalink',
+      content: m(CopyableLink, {
+        url: `${self.location.origin}/#!/record/${SHARE_SUBPAGE}/${hash}`,
+      }),
+    });
+  }
+
+  private async loadShared(hash: string) {
+    const url = `https://storage.googleapis.com/${BUCKET_NAME}/${hash}`;
+    const fetchData = await fetch(url);
+    const json = await fetchData.text();
+    const res = this.recMgr.restoreSessionFromJson(json);
+    if (!res.ok) {
+      showModal({title: 'Restore error', content: res.error});
+      return;
+    }
+    this.recMgr.app.navigate('#!/record/cmdline');
+  }
+}
+
+interface RecCtlAttrs {
+  recMgr: RecordingManager;
+}
+
+class RecordingCtl implements m.ClassComponent<RecCtlAttrs> {
+  private recMgr: RecordingManager;
+  private lastTarget?: RecordingTarget;
+
+  constructor({attrs}: m.CVnode<RecCtlAttrs>) {
+    this.recMgr = attrs.recMgr;
+  }
+
+  view() {
+    const target = this.recMgr.currentTarget;
+    if (this.lastTarget !== target) {
+      this.lastTarget = target;
+    }
+
+    const currentSession = this.recMgr.currentSession;
+    const recordingInProgress = currentSession?.inProgress;
+    if (recordingInProgress) {
+      // Update the ETA if the recording is in progress.
+      setTimeout(() => m.redraw(), 1000);
+    }
+    const eta: string | undefined = currentSession?.eta;
+    return m(
+      '.record-ctl',
+      m(Button, {
+        icon: 'cable',
+        title: 'Click to select another target',
+        onclick: () => this.recMgr.app.navigate('#!/record/target'),
+      }),
+      m(
+        '.record-target',
+        recordingInProgress
+          ? `Recording${eta ? ', ETA ' + eta : ''}`
+          : target?.name ?? 'No target selected',
+      ),
+      recordingInProgress
+        ? m(Button, {
+            icon: 'stop',
+            disabled: currentSession.state !== 'RECORDING',
+            iconFilled: true,
+            title: 'Stop',
+            className: 'rec',
+            onclick: () => {
+              currentSession.session?.stop();
+              this.recMgr.app.navigate('#!/record/target');
+            },
+          })
+        : m(Button, {
+            icon: 'not_started',
+            disabled: target === undefined,
+            iconFilled: true,
+            title: 'Start tracing',
+            className: 'rec',
+            onclick: () => {
+              this.recMgr.startTracing();
+              this.recMgr.app.navigate('#!/record/target');
+            },
+          }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts
new file mode 100644
index 0000000..e936abf
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/saved_configs.ts
@@ -0,0 +1,143 @@
+// 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 {RecordingManager} from '../recording_manager';
+import {RecordSubpage} from '../config/config_interfaces';
+import {SavedSessionSchema, RecordPluginSchema} from '../serialization_schema';
+import {assertExists} from '../../../base/logging';
+
+export function savedConfigsPage(recMgr: RecordingManager): RecordSubpage {
+  const savedConfigs = new Array<SavedSessionSchema>();
+
+  return {
+    kind: 'GLOBAL_PAGE',
+    id: 'configs',
+    icon: 'save',
+    title: 'Saved configs',
+    subtitle: 'Save, restore and export configs',
+    render() {
+      return m(SavedConfigsPage, {recMgr, savedConfigs});
+    },
+    serialize(state: RecordPluginSchema) {
+      state.savedSessions = [...savedConfigs];
+    },
+    deserialize(state: RecordPluginSchema) {
+      savedConfigs.splice(0);
+      savedConfigs.push(...state.savedSessions);
+    },
+  };
+}
+
+type RecMgrAttrs = {
+  recMgr: RecordingManager;
+  savedConfigs: Array<SavedSessionSchema>;
+};
+
+class SavedConfigsPage implements m.ClassComponent<RecMgrAttrs> {
+  private newConfigName = '';
+  private recMgr: RecordingManager;
+  private savedConfigs: Array<SavedSessionSchema>;
+
+  constructor({attrs}: m.CVnode<RecMgrAttrs>) {
+    this.recMgr = attrs.recMgr;
+    this.savedConfigs = attrs.savedConfigs;
+  }
+
+  view() {
+    const canSave =
+      this.newConfigName.length > 0 &&
+      this.savedConfigs.every((s) => s.name !== this.newConfigName);
+    return [
+      m('header', 'Save and load configurations'),
+      m('.input-config', [
+        m('input', {
+          value: this.newConfigName,
+          placeholder: 'Title for config',
+          oninput: (e: Event) => {
+            this.newConfigName = (e.target as HTMLInputElement).value;
+          },
+        }),
+        m(
+          'button',
+          {
+            class: 'config-button',
+            disabled: !canSave,
+            title: canSave
+              ? 'Save current config'
+              : 'Duplicate name, saving disabled',
+            onclick: () => {
+              this.savedConfigs.push({
+                name: this.newConfigName,
+                config: this.recMgr.serializeSession(),
+              });
+              this.newConfigName = '';
+            },
+          },
+          m('i.material-icons', 'save'),
+        ),
+      ]),
+      this.savedConfigs.map((s) => this.renderSavedSessions(s)),
+    ];
+  }
+
+  private renderSavedSessions(item: SavedSessionSchema) {
+    const self = this;
+    return m('.config', [
+      m('span.title-config', item.name),
+      m(
+        'button',
+        {
+          class: 'config-button',
+          title: 'Apply configuration settings',
+          onclick: () => {
+            this.recMgr.loadSession(item.config);
+          },
+        },
+        m('i.material-icons', 'file_upload'),
+      ),
+      m(
+        'button',
+        {
+          class: 'config-button',
+          title: 'Overwrite configuration with current settings',
+          onclick: () => {
+            const msg = `Overwrite config "${item.name}" with current settings?`;
+            if (!confirm(msg)) return;
+            const savedCfg = assertExists(
+              this.savedConfigs.find((s) => s.name === item.name),
+            );
+            savedCfg.config = this.recMgr.serializeSession();
+          },
+        },
+        m('i.material-icons', 'save'),
+      ),
+      m(
+        'button',
+        {
+          class: 'config-button',
+          title: 'Remove configuration',
+          onclick: () => {
+            const idx = this.savedConfigs.findIndex(
+              (s) => s.name === item.name,
+            );
+            if (idx < 0) return;
+            self.savedConfigs.splice(idx, 1);
+          },
+        },
+        m('i.material-icons', 'delete'),
+      ),
+    ]);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts
new file mode 100644
index 0000000..a30069a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/stack_sampling.ts
@@ -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.
+
+import {splitLinesNonEmpty} from '../../../base/string_utils';
+import protos from '../../../protos';
+import {RecordProbe, RecordSubpage} from '../config/config_interfaces';
+import {TraceConfigBuilder} from '../config/trace_config_builder';
+import {Slider} from './widgets/slider';
+import {Textarea} from './widgets/textarea';
+
+export function stackSamplingRecordSection(): RecordSubpage {
+  return {
+    kind: 'PROBES_PAGE',
+    id: 'stack_sampling',
+    title: 'Stack sampling',
+    subtitle: 'Lightweight cpu profiling',
+    icon: 'full_stacked_bar_chart',
+    probes: [tracedPerf()],
+  };
+}
+
+function tracedPerf(): RecordProbe {
+  const settings = {
+    samplingFreq: new Slider({
+      title: 'Sampling frequency',
+      cssClass: '.thin',
+      default: 100,
+      values: [1, 10, 50, 100, 250, 500, 1000],
+      unit: 'Hz',
+    }),
+    procs: new Textarea({
+      placeholder:
+        'Filters for processes to profile, one per line e.g.' +
+        'com.android.phone\nlmkd\ncom.android.webview:sandboxed_process*',
+    }),
+  };
+  return {
+    id: 'traced_perf',
+    title: 'Callstack sampling',
+    image: 'rec_profiling.png',
+    description:
+      'Periodically records the current callstack (chain of ' +
+      'function calls) of processes.',
+    supportedPlatforms: ['ANDROID', 'LINUX'],
+    settings,
+    genConfig: function (tc: TraceConfigBuilder) {
+      const s = settings;
+      const pkgs = splitLinesNonEmpty(s.procs.text);
+      tc.addDataSource('linux.perf').perfEventConfig = {
+        timebase: {
+          frequency: s.samplingFreq.value,
+          timestampClock: protos.PerfEvents.PerfClock.PERF_CLOCK_MONOTONIC,
+        },
+        callstackSampling: {
+          scope:
+            pkgs.length > 0
+              ? {
+                  targetCmdline: pkgs,
+                }
+              : undefined,
+        },
+      };
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts
new file mode 100644
index 0000000..20b86fb
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/target_selection_page.ts
@@ -0,0 +1,415 @@
+// 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 {RecordingTarget} from '../interfaces/recording_target';
+import {SegmentedButtons} from '../../../widgets/segmented_buttons';
+import {TARGET_PLATFORMS} from '../interfaces/target_platform';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {Icon} from '../../../widgets/icon';
+import {Button} from '../../../widgets/button';
+import {Intent} from '../../../widgets/common';
+import {getOrCreate} from '../../../base/utils';
+import {PreflightCheckRenderer} from './preflight_check_renderer';
+import {Select} from '../../../widgets/select';
+import {DisposableStack} from '../../../base/disposable_stack';
+import {CurrentTracingSession, RecordingManager} from '../recording_manager';
+import {downloadData} from '../../../base/download_utils';
+import {RecordSubpage} from '../config/config_interfaces';
+import {RecordPluginSchema} from '../serialization_schema';
+import {Checkbox} from '../../../widgets/checkbox';
+
+type RecMgrAttrs = {recMgr: RecordingManager};
+
+export function targetSelectionPage(recMgr: RecordingManager): RecordSubpage {
+  return {
+    kind: 'GLOBAL_PAGE',
+    id: 'target',
+    icon: 'cable',
+    title: 'Target device',
+    subtitle: 'Live recording via USB/WebSocket',
+    render() {
+      return m(TargetSelectionPage, {recMgr});
+    },
+    serialize(state: RecordPluginSchema) {
+      state.target = {
+        platformId: recMgr.currentPlatform,
+        transportId: recMgr.currentProvider?.id,
+        targetId: recMgr.currentTarget?.id,
+      };
+      state.autoOpenTrace = recMgr.autoOpenTraceWhenTracingEnds;
+    },
+    async deserialize(state: RecordPluginSchema) {
+      recMgr.autoOpenTraceWhenTracingEnds = state.autoOpenTrace;
+      if (state.target.platformId === undefined) return;
+      recMgr.setPlatform(state.target.platformId);
+      const prov = recMgr.getProvider(state.target.transportId ?? '');
+      if (prov === undefined) return;
+      await recMgr.setProvider(prov);
+      if (state.target.targetId === undefined) return;
+      for (const target of await recMgr.listTargets()) {
+        if (target.id === state.target.targetId) {
+          await recMgr.setTarget(target);
+        }
+      }
+    },
+  };
+}
+
+class TargetSelectionPage implements m.ClassComponent<RecMgrAttrs> {
+  view({attrs}: m.CVnode<RecMgrAttrs>) {
+    return [
+      m('header', 'Select platform'),
+      m(SegmentedButtons, {
+        className: 'platform-selector',
+        options: TARGET_PLATFORMS.map((p) => ({label: p.name, icon: p.icon})),
+        selectedOption: TARGET_PLATFORMS.findIndex(
+          (p) => p.id === attrs.recMgr.currentPlatform,
+        ),
+        onOptionSelected: (num) => {
+          attrs.recMgr.setPlatform(TARGET_PLATFORMS[num].id);
+          // m.redraw();
+        },
+      }),
+      [
+        m(TransportSelector, {
+          recMgr: attrs.recMgr,
+          key: attrs.recMgr.currentPlatform,
+        }),
+      ],
+    ];
+  }
+}
+
+class TransportSelector implements m.ClassComponent<RecMgrAttrs> {
+  private transportKeys = new ObjToId();
+
+  view({attrs}: m.CVnode<RecMgrAttrs>) {
+    const options = [];
+    for (const provider of attrs.recMgr.listProvidersForCurrentPlatform()) {
+      const id = this.transportKeys.getKey(provider);
+      options.push([
+        m(`input[type=radio][name=recordingProvider][id=${id}]`, {
+          onchange: async () => {
+            await attrs.recMgr.setProvider(provider);
+            m.redraw();
+          },
+          checked: attrs.recMgr.currentProvider === provider,
+        }),
+        m(
+          `label[for=${id}]`,
+          m(Icon, {icon: provider.icon}),
+          m('.title', provider.name),
+          m('.description', provider.description),
+        ),
+      ]);
+    }
+    return [
+      m('header', 'Select transport'),
+      m('fieldset.record-transports', ...options),
+      attrs.recMgr.currentProvider && [
+        m(TargetSelector, {
+          recMgr: attrs.recMgr,
+          provider: attrs.recMgr.currentProvider,
+          key: this.transportKeys.getKey(attrs.recMgr.currentProvider),
+        }),
+      ],
+    ];
+  }
+}
+
+type TargetSelectorAttrs = {
+  recMgr: RecordingManager;
+  provider: RecordingTargetProvider;
+};
+class TargetSelector implements m.ClassComponent<TargetSelectorAttrs> {
+  private targetIdMap = new ObjToId();
+  private checksRenderer: PreflightCheckRenderer;
+  private trash = new DisposableStack();
+  private targets: RecordingTarget[] = [];
+  private provider: RecordingTargetProvider;
+  private recMgr: RecordingManager;
+
+  constructor({attrs}: m.CVnode<TargetSelectorAttrs>) {
+    this.recMgr = attrs.recMgr;
+    this.provider = attrs.provider;
+    this.checksRenderer = new PreflightCheckRenderer(attrs.provider);
+    this.trash.use(
+      attrs.provider.onTargetsChanged.addListener(() => this.refreshTargets()),
+    );
+    this.checksRenderer
+      .runPreflightChecks() //
+      .then(() => this.refreshTargets());
+    this.recMgr.listTargets().then((targets) => {
+      this.targets = targets;
+      m.redraw();
+    });
+  }
+
+  view({attrs}: m.CVnode<TargetSelectorAttrs>) {
+    const recMgr = attrs.recMgr;
+    return [
+      this.checksRenderer.renderTable(),
+      m('header', 'Select target device'),
+
+      m(
+        '.record-targets',
+        m(
+          Select,
+          {
+            onchange: (e: Event) => {
+              const idx = (e.target as HTMLSelectElement).selectedIndex;
+              recMgr.setTarget(this.targets[idx]);
+              // m.redraw();
+            },
+          },
+          ...this.targets.map((target) =>
+            m(
+              'option',
+              {selected: recMgr.currentTarget === target},
+              target.name,
+            ),
+          ),
+        ),
+        m(Button, {
+          icon: 'refresh',
+          title: 'Refresh devices',
+          onclick: () => {
+            // This forces the TargetDetails component to be re-initialized,
+            // in turn causing the pre-flight checks to be repeated. UX-wise
+            // we want the refresh button to both reload the target list and
+            // also reload the current target.
+            this.targetIdMap.clear();
+            this.refreshTargets();
+          },
+        }),
+        recMgr.currentTarget &&
+          m(Button, {
+            icon: recMgr.currentTarget.connected ? 'cancel' : 'power_off',
+            iconFilled: true,
+            disabled: !recMgr.currentTarget.connected,
+            title: recMgr.currentTarget.connected
+              ? 'Disconnect the current device'
+              : 'Device disconnected',
+            onclick: () => recMgr.currentTarget?.disconnect(),
+          }),
+        attrs.provider.pairNewTarget &&
+          m(Button, {
+            label: 'Connect new device',
+            icon: 'add',
+            intent: Intent.Primary,
+            onclick: async () => {
+              const target = await attrs.provider.pairNewTarget!();
+              target && recMgr.setTarget(target);
+              await this.refreshTargets();
+            },
+          }),
+      ),
+      recMgr.currentTarget && [
+        m(TargetDetails, {
+          recMgr: attrs.recMgr,
+          target: recMgr.currentTarget,
+          key: this.targetIdMap.getKey(recMgr.currentTarget),
+        }),
+      ],
+    ];
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+
+  private async refreshTargets() {
+    // Re-triggers refresh and auto-select first valid target.
+    this.recMgr.setProvider(this.provider);
+    this.targets = await this.recMgr.listTargets();
+    m.redraw();
+  }
+}
+
+type TargetDetailsAttrs = {recMgr: RecordingManager; target: RecordingTarget};
+class TargetDetails implements m.ClassComponent<TargetDetailsAttrs> {
+  private checksRenderer?: PreflightCheckRenderer;
+
+  constructor({attrs}: m.CVnode<TargetDetailsAttrs>) {
+    this.checksRenderer = new PreflightCheckRenderer(attrs.target);
+    this.checksRenderer.runPreflightChecks();
+  }
+
+  view({attrs}: m.CVnode<TargetDetailsAttrs>) {
+    return [
+      this.checksRenderer?.renderTable(),
+      m(SessionMgmtRenderer, {recMgr: attrs.recMgr, target: attrs.target}),
+    ];
+  }
+}
+
+type SessionMgmtAttrs = {recMgr: RecordingManager; target: RecordingTarget};
+class SessionMgmtRenderer implements m.ClassComponent<SessionMgmtAttrs> {
+  view({attrs}: m.CVnode<SessionMgmtAttrs>) {
+    const session = attrs.recMgr.currentSession;
+    const isRecording = session?.state === 'RECORDING';
+    return [
+      m('header', 'Tracing session'),
+      m(
+        'div',
+        m(Button, {
+          label: 'Start tracing',
+          icon: 'not_started',
+          iconFilled: true,
+          className: 'start',
+          disabled: isRecording,
+          onclick: () => attrs.recMgr.startTracing().then(() => m.redraw()),
+        }),
+        m(Button, {
+          label: 'Stop',
+          icon: 'stop',
+          className: 'stop',
+          iconFilled: true,
+          disabled: !isRecording,
+          onclick: () => session?.session?.stop().then(() => m.redraw()),
+        }),
+        m(Button, {
+          label: 'Cancel',
+          icon: 'cancel',
+          className: 'cancel',
+          iconFilled: true,
+          disabled: !isRecording,
+          onclick: () => session?.session?.cancel().then(() => m.redraw()),
+        }),
+        m(Checkbox, {
+          label: 'Open trace when done',
+          checked: attrs.recMgr.autoOpenTraceWhenTracingEnds,
+          onchange: (e) => {
+            attrs.recMgr.autoOpenTraceWhenTracingEnds = Boolean(
+              (e.target as HTMLInputElement).checked,
+            );
+          },
+        }),
+      ),
+      session?.error && m('div', session.error),
+      session && [
+        m(SessionStateRenderer, {
+          session,
+          key: session.uuid,
+        }),
+      ],
+    ];
+  }
+}
+
+type SessionStateAttrs = {
+  session: CurrentTracingSession;
+};
+class SessionStateRenderer implements m.ClassComponent<SessionStateAttrs> {
+  private session: CurrentTracingSession;
+  private trash = new DisposableStack();
+  private bufferUsagePct = 'N/A';
+
+  constructor({attrs}: m.CVnode<SessionStateAttrs>) {
+    this.session = attrs.session;
+    this.trash.use(this.pollBufferState());
+  }
+
+  private pollBufferState(): Disposable {
+    const timerId = window.setInterval(async () => {
+      const bufferUsagePct = await this.session.session?.getBufferUsagePct();
+      if (bufferUsagePct !== undefined) {
+        // Retain the last valid buffer usage in the dialog, so the user can
+        // get a sense of overruns even after the trace ends.
+        this.bufferUsagePct = `${bufferUsagePct} %`;
+      }
+      m.redraw();
+    }, 1000);
+    return {
+      [Symbol.dispose]() {
+        window.clearInterval(timerId);
+      },
+    };
+  }
+
+  view() {
+    const traceData = this.session.isCompleted
+      ? this.session.session?.getTraceData()
+      : undefined;
+    const logs = this.getLogs();
+    const eta = this.session.eta;
+    return m(
+      'table.session-status',
+      m('tr', m('td', 'State'), m('td', this.session.state)),
+      m('tr', m('td', 'Buffer usage'), m('td', this.bufferUsagePct)),
+      eta && m('tr', m('td', 'ETA'), m('td', eta)),
+      traceData &&
+        m(
+          'tr',
+          m('td', 'Trace file'),
+          m(
+            'td',
+            `${Math.round(traceData.length / 1e3).toLocaleString()} KB`,
+            this.session.isCompressed && ' (compressed)',
+            m(Button, {
+              label: 'Open',
+              icon: 'file_open',
+              onclick: () => this.session.openTrace(),
+            }),
+            m(Button, {
+              label: 'Download',
+              icon: 'download',
+              onclick: () => downloadData(this.session.fileName, traceData),
+            }),
+          ),
+        ),
+      logs != '' && m('tr', m('td', 'Logs'), m('td', m('pre.logs', logs))),
+    );
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+
+  private getLogs() {
+    let log = '';
+    for (const l of this.session.session?.logs ?? []) {
+      const timestamp = l.timestamp.toTimeString().substring(0, 8);
+      log += `${timestamp}: ${l.message}\n`;
+    }
+    return log;
+  }
+}
+
+/**
+ * A utility class to assign unique string IDs to object instances.
+ * This is used to generate the key: attr for mithril, for components that take
+ * an object instance as attr, to ensure that mithril instantiates a new
+ * component when the input object changes.
+ * Example:
+ * let obj = new MyFoo();
+ * const map = new ObjId();
+ * console.log(map.getKey(obj));  // Prints 'obj_1'.
+ * console.log(map.getKey(obj));  // Prints 'obj_1'.
+ * obj = new MyFoo();
+ * console.log(map.getKey(obj));  // Prints 'obj_2'.
+ */
+export class ObjToId {
+  private map = new WeakMap<object, string>();
+  private lastId = 0;
+
+  getKey(obj: object): string {
+    return getOrCreate(this.map, obj, () => `obj_${++this.lastId}`);
+  }
+
+  clear() {
+    this.map = new WeakMap<object, string>();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts
new file mode 100644
index 0000000..cc0c258
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/docs_chip.ts
@@ -0,0 +1,30 @@
+// 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';
+
+interface DocsChipAttrs {
+  href: string;
+}
+
+export class DocsChip implements m.ClassComponent<DocsChipAttrs> {
+  view({attrs}: m.CVnode<DocsChipAttrs>) {
+    return m(
+      'a.inline-chip',
+      {href: attrs.href, title: 'Open docs in new tab', target: '_blank'},
+      m('i.material-icons', 'info'),
+      ' Docs',
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts
new file mode 100644
index 0000000..c3feefc
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/multiselect.ts
@@ -0,0 +1,84 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+import {MultiSelect, MultiSelectDiff} from '../../../../widgets/multiselect';
+
+export interface CheckboxesAttrs<T> {
+  title?: string;
+  options: Map<string, T>;
+  onChange?: (options: string[]) => void;
+}
+
+export class TypedMultiselect<T> implements ProbeSetting {
+  private _selectedKeys = new Set<string>();
+
+  constructor(readonly attrs: CheckboxesAttrs<T>) {}
+
+  setEnabled(key: string, enabled: boolean) {
+    if (enabled) {
+      this._selectedKeys.add(key);
+    } else {
+      this._selectedKeys.delete(key);
+    }
+  }
+
+  selectedKeys(): string[] {
+    return Array.from(this._selectedKeys);
+  }
+
+  selectedValues(): T[] {
+    const values = [];
+    for (const [key, value] of this.attrs.options.entries()) {
+      if (this._selectedKeys.has(key)) {
+        values.push(value);
+      }
+    }
+    return values;
+  }
+
+  serialize() {
+    return Array.from(this._selectedKeys);
+  }
+
+  deserialize(state: unknown): void {
+    if (Array.isArray(state) && state.every((x) => typeof x === 'string')) {
+      this._selectedKeys.clear();
+      for (const key of state) {
+        this.attrs.options.has(key) && this._selectedKeys.add(key);
+      }
+    }
+  }
+
+  render() {
+    return [
+      this.attrs.title && m('header', this.attrs.title),
+      m(MultiSelect, {
+        fixedSize: true,
+        options: Array.from(this.attrs.options.keys()).map((key) => ({
+          id: key,
+          name: key,
+          checked: this._selectedKeys.has(key),
+        })),
+        onChange: (diffs: MultiSelectDiff[]) => {
+          for (const diff of diffs) {
+            this.setEnabled(diff.id, diff.checked);
+          }
+          this.attrs.onChange?.(Array.from(this._selectedKeys.values()));
+        },
+      }),
+    ];
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts
new file mode 100644
index 0000000..1d063ce
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/slider.ts
@@ -0,0 +1,142 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+import {assertTrue} from '../../../../base/logging';
+import {exists} from '../../../../base/utils';
+
+export interface SliderAttrs {
+  title: string;
+  values: number[];
+  default?: number;
+  icon?: string;
+  cssClass?: string;
+  isTime?: boolean;
+  unit: string;
+  min?: number;
+  description?: string;
+  disabled?: boolean;
+  zeroIsDefault?: boolean;
+  onChange?: (value: number) => void;
+}
+
+export class Slider implements ProbeSetting {
+  private _value: number;
+
+  constructor(readonly attrs: SliderAttrs) {
+    assertTrue(attrs.values.length > 0);
+    this._value = this.setValue(undefined);
+  }
+
+  serialize() {
+    return this._value;
+  }
+
+  deserialize(state: unknown): void {
+    if (typeof state === 'number') {
+      this._value = state;
+    }
+  }
+
+  get value(): number {
+    return this._value;
+  }
+
+  setValue(value: number | null | undefined) {
+    // Logic if value is null/undefined: try first the .default, if provided,
+    // otherwise fall back on the first value of the fixed range... otherwise 0.
+    this._value = exists(value)
+      ? value
+      : this.attrs.default ?? this.attrs.values[0] ?? 0;
+    return this._value;
+  }
+
+  private onValueChange(newVal: number) {
+    this._value = newVal;
+    this.attrs.onChange?.(newVal);
+  }
+
+  onTimeValueChange(hms: string) {
+    try {
+      const date = new Date(`1970-01-01T${hms}.000Z`);
+      if (isNaN(date.getTime())) return;
+      this.onValueChange(date.getTime());
+    } catch {}
+  }
+
+  onSliderChange(newIdx: number) {
+    this.onValueChange(this.attrs.values[newIdx]);
+  }
+
+  render() {
+    const attrs = this.attrs;
+    const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase();
+    const maxIdx = attrs.values.length - 1;
+    const val = this._value;
+    let min = attrs.min ?? 1;
+    if (attrs.zeroIsDefault) {
+      min = Math.min(0, min);
+    }
+    const description = attrs.description;
+    const disabled = attrs.disabled;
+
+    // Find the index of the closest value in the slider.
+    let idx = 0;
+    for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {}
+
+    let spinnerCfg = {};
+    if (attrs.isTime) {
+      spinnerCfg = {
+        type: 'text',
+        pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss
+        value: new Date(val).toISOString().substring(11, 11 + 8),
+        oninput: (e: InputEvent) => {
+          this.onTimeValueChange((e.target as HTMLInputElement).value);
+        },
+      };
+    } else {
+      const isDefault = attrs.zeroIsDefault && val === 0;
+      spinnerCfg = {
+        type: 'number',
+        value: isDefault ? '' : val,
+        placeholder: isDefault ? '(default)' : '',
+        oninput: (e: InputEvent) => {
+          this.onValueChange(+(e.target as HTMLInputElement).value);
+        },
+      };
+    }
+    return m(
+      '.slider' + (attrs.cssClass ?? ''),
+      m('header', attrs.title),
+      description ? m('header.descr', attrs.description) : '',
+      attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
+      m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, {
+        disabled,
+        oninput: (e: InputEvent) => {
+          this.onSliderChange(+(e.target as HTMLInputElement).value);
+        },
+      }),
+      m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
+      m('.unit', attrs.unit),
+    );
+  }
+}
+
+export const POLL_INTERVAL_SLIDER: SliderAttrs = {
+  title: 'Poll interval',
+  values: [250, 500, 1000, 2500, 5000, 30000, 60000],
+  cssClass: '.thin',
+  unit: 'ms',
+};
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts
new file mode 100644
index 0000000..18eb496
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/textarea.ts
@@ -0,0 +1,72 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+import {DocsChip} from './docs_chip';
+
+export interface TextareaAttrs {
+  placeholder: string;
+  title?: string;
+  docsLink?: string;
+  cssClass?: string;
+  default?: string;
+  onChange?: (text: string) => void;
+}
+
+export class Textarea implements ProbeSetting {
+  private _text: string;
+
+  constructor(readonly attrs: TextareaAttrs) {
+    this._text = this.setText(attrs.default); // re-assignment to make tsc happy.
+  }
+
+  setText(text: string | undefined) {
+    this._text = text ?? '';
+    return this._text;
+  }
+
+  get text(): string {
+    return this._text;
+  }
+
+  serialize() {
+    return this._text;
+  }
+
+  deserialize(state: unknown): void {
+    if (typeof state === 'string') {
+      this._text = state;
+    }
+  }
+
+  render() {
+    return m(
+      '.textarea-holder',
+      m(
+        'header',
+        this.attrs.title,
+        this.attrs.docsLink && [' ', m(DocsChip, {href: this.attrs.docsLink})],
+      ),
+      m(`textarea.extra-input${this.attrs.cssClass ?? ''}`, {
+        onchange: (e: Event) => {
+          this.setText((e.target as HTMLTextAreaElement).value);
+          this.attrs.onChange?.(this._text);
+        },
+        placeholder: this.attrs.placeholder,
+        value: this._text,
+      }),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts
new file mode 100644
index 0000000..94f2c38
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/pages/widgets/toggle.ts
@@ -0,0 +1,69 @@
+// 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 {ProbeSetting} from '../../config/config_interfaces';
+
+export interface ToggleAttrs {
+  title: string;
+  descr?: string;
+  default?: boolean;
+  cssClass?: string;
+  onChange?: (enabled: boolean) => void;
+}
+
+export class Toggle implements ProbeSetting {
+  private _enabled: boolean;
+
+  constructor(readonly attrs: ToggleAttrs) {
+    this._enabled = this.setEnabled(undefined);
+  }
+
+  setEnabled(enabled: boolean | undefined) {
+    this._enabled = enabled ?? this.attrs.default ?? false;
+    return this._enabled;
+  }
+
+  get enabled(): boolean {
+    return this._enabled;
+  }
+
+  serialize() {
+    return this._enabled;
+  }
+
+  deserialize(state: unknown): void {
+    if (state === true || state === false) {
+      this._enabled = state;
+    }
+  }
+
+  render() {
+    return m(
+      `.toggle${this._enabled ? '.enabled' : ''}${this.attrs.cssClass ?? ''}`,
+      m(
+        'label',
+        m(`input[type=checkbox]`, {
+          checked: this._enabled,
+          oninput: (e: InputEvent) => {
+            this.setEnabled((e.target as HTMLInputElement).checked);
+            this.attrs.onChange?.(this._enabled);
+          },
+        }),
+        m('span', this.attrs.title),
+      ),
+      m('.descr', this.attrs.descr),
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts
new file mode 100644
index 0000000..6803066
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/recording_manager.ts
@@ -0,0 +1,323 @@
+// 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 protos from '../../protos';
+import {assertFalse, assertTrue} from '../../base/logging';
+import {errResult, okResult, Result} from '../../base/result';
+import {App} from '../../public/app';
+import {RecordSubpage} from './config/config_interfaces';
+import {ConfigManager} from './config/config_manager';
+import {RecordingTarget} from './interfaces/recording_target';
+import {RecordingTargetProvider} from './interfaces/recording_target_provider';
+import {
+  RECORD_PLUGIN_SCHEMA,
+  RECORD_SESSION_SCHEMA,
+  RecordPluginSchema,
+  RecordSessionSchema,
+} from './serialization_schema';
+import {TargetPlatformId} from './interfaces/target_platform';
+import {TracingSession} from './interfaces/tracing_session';
+import {GcsUploader} from '../../base/gcs_uploader';
+import {uuidv4} from '../../base/uuid';
+import {Time, Timecode} from '../../base/time';
+
+const LOCALSTORAGE_KEY = 'recordPlugin';
+
+export class RecordingManager {
+  readonly pages = new Map<string, RecordSubpage>();
+
+  private providers = new Array<RecordingTargetProvider>();
+  private platform: TargetPlatformId = 'ANDROID';
+  private provider?: RecordingTargetProvider;
+  private target?: RecordingTarget;
+  private _tracingSession?: CurrentTracingSession;
+  readonly recordConfig = new ConfigManager();
+  autoOpenTraceWhenTracingEnds = true;
+
+  constructor(readonly app: App) {}
+
+  registerPage(...pages: RecordSubpage[]) {
+    for (const page of pages) {
+      assertTrue(!this.pages.has(page.id) || this.pages.get(page.id) === page);
+      this.pages.set(page.id, page);
+      if (page.kind === 'PROBES_PAGE') {
+        this.recordConfig.registerProbes(page.probes);
+      }
+    }
+  }
+
+  registerProvider(provider: RecordingTargetProvider) {
+    assertFalse(this.providers.includes(provider));
+    this.providers.push(provider);
+  }
+
+  get currentPlatform(): TargetPlatformId {
+    return this.platform;
+  }
+
+  setPlatform(platform: TargetPlatformId) {
+    this.platform = platform;
+    this.provider = undefined;
+    this.target = undefined;
+    // If there is only one provider for the platform, auto-select that.
+    const filteredProviders = this.listProvidersForCurrentPlatform();
+    if (filteredProviders.length === 1) {
+      this.provider = filteredProviders[0];
+    }
+  }
+
+  listProvidersForCurrentPlatform(): RecordingTargetProvider[] {
+    return this.providers.filter((p) =>
+      p.supportedPlatforms.includes(this.platform),
+    );
+  }
+
+  get currentProvider(): RecordingTargetProvider | undefined {
+    return this.provider;
+  }
+
+  getProvider(id: string): RecordingTargetProvider | undefined {
+    return this.providers.find((p) => p.id === id);
+  }
+
+  async setProvider(provider: RecordingTargetProvider) {
+    if (!provider.supportedPlatforms.includes(this.currentPlatform)) {
+      // This can happen if the promise that calls refreshTargets() completes
+      // after the user has switched to a different platform.
+      return;
+    }
+    this.provider = provider;
+    const targets = await provider.listTargets(this.currentPlatform);
+    if (this.target && targets.includes(this.target)) {
+      return; // The currently selected target is still valid, retain it.
+    }
+    this.target = targets.length > 0 ? targets[0] : undefined;
+    this.app.raf.scheduleFullRedraw();
+  }
+
+  async listTargets(): Promise<RecordingTarget[]> {
+    if (this.provider === undefined) return [];
+    return await this.provider.listTargets(this.currentPlatform);
+  }
+
+  get currentSession() {
+    return this._tracingSession;
+  }
+
+  setTarget(target: RecordingTarget) {
+    this.target = target;
+  }
+
+  get currentTarget(): RecordingTarget | undefined {
+    return this.target;
+  }
+
+  genTraceConfig(): protos.TraceConfig {
+    return this.recordConfig.genTraceConfig(this.currentPlatform);
+  }
+
+  async startTracing(): Promise<CurrentTracingSession> {
+    if (this._tracingSession !== undefined) {
+      this._tracingSession.session?.cancel();
+      this._tracingSession = undefined;
+    }
+    const traceCfg = this.genTraceConfig();
+    const wrappedSession = new CurrentTracingSession(this, traceCfg);
+    this._tracingSession = wrappedSession;
+    return wrappedSession;
+  }
+
+  async share(): Promise<string> {
+    const config = this.serializeSession();
+    const json = JSON.stringify(config);
+    const uploader = new GcsUploader(json, {mimeType: 'application/json'});
+    await uploader.waitForCompletion();
+    return uploader.uploadedUrl;
+  }
+
+  serializeSession(): RecordSessionSchema {
+    // Initialize with default values.
+    const state: RecordSessionSchema = RECORD_SESSION_SCHEMA.parse({});
+    for (const page of this.pages.values()) {
+      if (page.kind === 'SESSION_PAGE') {
+        page.serialize(state);
+      }
+    }
+    // Serialize the state of each probe page and their settings.
+    state.probes = this.recordConfig.serializeProbes();
+    return state;
+  }
+
+  loadSession(state: RecordSessionSchema): void {
+    for (const page of this.pages.values()) {
+      if (page.kind === 'SESSION_PAGE') {
+        page.deserialize(state);
+      }
+    }
+    this.recordConfig.deserializeProbes(state.probes);
+  }
+
+  persistIntoLocalStorage(): void {
+    const state: RecordPluginSchema = RECORD_PLUGIN_SCHEMA.parse({});
+    state.lastSession = this.serializeSession();
+    for (const page of this.pages.values()) {
+      if (page.kind === 'GLOBAL_PAGE') {
+        page.serialize(state);
+      }
+    }
+    const json = JSON.stringify(state);
+    localStorage.setItem(LOCALSTORAGE_KEY, json);
+  }
+
+  restorePluginStateFromLocalstorage(): void {
+    const stateJson = localStorage.getItem(LOCALSTORAGE_KEY) ?? '{}';
+    let parsedJson: unknown;
+    try {
+      parsedJson = JSON.parse(stateJson);
+    } catch (e) {
+      console.error('Record plugin: JSON parse failed', e);
+      parsedJson = {};
+    }
+    const res = RECORD_PLUGIN_SCHEMA.safeParse(parsedJson);
+    if (!res.success) {
+      throw new Error('Record plugin: deserialization failed', res.error);
+    }
+    const state = res.data;
+    for (const page of this.pages.values()) {
+      if (page.kind === 'GLOBAL_PAGE') {
+        page.deserialize(state);
+      }
+    }
+    if (state.lastSession !== undefined) {
+      this.loadSession(state.lastSession);
+    }
+  }
+
+  restoreSessionFromJson(json: string): Result<void> {
+    let parsedJson: unknown;
+    try {
+      parsedJson = JSON.parse(json);
+    } catch (e) {
+      return errResult(`JSON parser error: ${e.message}`);
+    }
+    const res = RECORD_SESSION_SCHEMA.safeParse(parsedJson);
+    if (!res.success) {
+      return errResult(`Deserialization error: ${res.error}`);
+    }
+    this.loadSession(res.data);
+    return okResult(undefined);
+  }
+
+  clearSession() {
+    const emptySession = RECORD_SESSION_SCHEMA.parse({});
+    return this.loadSession(emptySession);
+  }
+}
+
+export class CurrentTracingSession {
+  error?: string;
+  session?: TracingSession;
+  readonly uuid = uuidv4();
+  readonly fileName: string;
+  readonly isCompressed: boolean;
+  private _expectedEndTime: number | undefined;
+  private recMgr: RecordingManager;
+  private autoOpenedTriggered = false;
+
+  constructor(recMgr: RecordingManager, traceCfg: protos.TraceConfig) {
+    this.recMgr = recMgr;
+    const now = new Date();
+    const ymd = `${now.getFullYear()}${now.getMonth()}${now.getDay()}`;
+    const hms = `${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
+    const platLowerCase = recMgr.currentPlatform.toLowerCase();
+    this.fileName = `${platLowerCase}-${ymd}-${hms}.pftrace`;
+    this.isCompressed = traceCfg.compressionType !== 0;
+    if (recMgr.currentTarget === undefined) {
+      this.error = 'No target selected';
+      return;
+    }
+    if (recMgr.currentTarget.emitsCompressedtrace) {
+      this.fileName += '.gz';
+      this.isCompressed = true;
+    }
+    this.start(traceCfg, recMgr.currentTarget);
+  }
+
+  async start(traceCfg: protos.TraceConfig, target: RecordingTarget) {
+    const res = await target.startTracing(traceCfg);
+    this.recMgr.app.raf.scheduleFullRedraw();
+    if (!res.ok) {
+      this.error = res.error;
+      return;
+    }
+    const session = (this.session = res.value);
+
+    if (traceCfg.durationMs > 0) {
+      this._expectedEndTime = performance.now() + traceCfg.durationMs;
+    }
+
+    session.onSessionUpdate.addListener(() => {
+      this.recMgr.app.raf.scheduleFullRedraw();
+      if (
+        session.state === 'FINISHED' &&
+        this.recMgr.autoOpenTraceWhenTracingEnds &&
+        !this.autoOpenedTriggered
+      ) {
+        this.autoOpenedTriggered = true;
+        this.openTrace();
+      }
+    });
+  }
+
+  get state(): string {
+    if (this.error !== undefined) {
+      return `Error: ${this.error}`;
+    }
+    if (this.session === undefined) {
+      return 'Initializing';
+    }
+    return this.session.state;
+  }
+
+  get eta(): string | undefined {
+    if (this._expectedEndTime === undefined) return undefined;
+    let remainingMs = Math.max(this._expectedEndTime - performance.now(), 0);
+    if (['FINISHED', 'ERRORED'].includes(this.session?.state ?? '')) {
+      remainingMs = 0;
+    }
+    return new Timecode(Time.fromMillis(remainingMs)).dhhmmss;
+  }
+
+  openTrace() {
+    const traceData: Uint8Array | undefined = this.session?.getTraceData();
+    if (traceData === undefined) return;
+    this.recMgr.app.openTraceFromBuffer({
+      buffer: traceData,
+      title: this.fileName,
+      fileName: this.fileName,
+    });
+  }
+
+  get isCompleted(): boolean {
+    return this.session?.state === 'FINISHED';
+  }
+
+  get inProgress(): boolean {
+    return (
+      (this.session === undefined && this.error === undefined) ||
+      this.session?.state === 'RECORDING' ||
+      this.session?.state === 'STOPPING'
+    );
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts
new file mode 100644
index 0000000..82ec10f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/serialization_schema.ts
@@ -0,0 +1,94 @@
+// 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 {z} from 'zod';
+import {TARGET_PLATFORMS, TargetPlatformId} from './interfaces/target_platform';
+
+// Overall view
+// RECORD_PLUGIN_SCHEMA:
+//   target: TARGET_SCHEMA
+//   lastSession: RECORD_SESSION_SCHEMA
+//      probes: PROBES_SCHEMA{}
+//   savedSessions: Array<RECORD_SESSION_SCHEMA>
+//      probes: PROBES_SCHEMA{}
+
+// Holds the state of the PROBES_PAGE subpages (e.g., Memory).
+// We don't define a strongly-typed schema for each probes as they are
+// changed frequently. Each probe is modelled as:
+// - An enable/disable boolean (the presence of the key)
+// - A map of "settings". Each setting widget (Slider, Textarea, Toggle)
+//   takes care of its own de/serialization.
+export const PROBES_SCHEMA = z
+  .record(
+    z.string(), // key: the RecordProbe.id (it's globally unique).
+    z.object({
+      settings: z
+        .record(
+          z.string(), // key: the key in the RecordProbe.settings map.
+          z.unknown(), // value: The result of ProbeSetting.serialize().
+        )
+        .default({}),
+    }),
+  )
+  .default({});
+export type ProbesSchema = z.infer<typeof PROBES_SCHEMA>;
+
+// The schema that holds the settings for a recording session, that is, the
+// state of the probes and the buffer size & type.
+// This does NOT include the state of the other recording pages (e.g. the
+// Target device selector, the "saved sessions", etc)
+export const RECORD_SESSION_SCHEMA = z
+  .object({
+    mode: z
+      .enum(['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'])
+      .default('STOP_WHEN_FULL'),
+    bufSizeKb: z.number().default(64 * 1024),
+    durationMs: z.number().default(10_000),
+    maxFileSizeMb: z.number().default(500),
+    fileWritePeriodMs: z.number().default(2500),
+    compression: z.boolean().default(false),
+    probes: PROBES_SCHEMA,
+  })
+  .default({});
+export type RecordSessionSchema = z.infer<typeof RECORD_SESSION_SCHEMA>;
+
+// The schema for the target selection page.
+export const TARGET_SCHEMA = z
+  .object({
+    platformId: z
+      .enum(TARGET_PLATFORMS.map((p) => p.id) as [TargetPlatformId])
+      .optional(),
+    transportId: z.string().optional(),
+    targetId: z.string().optional(),
+  })
+  .default({});
+export type TargetSchema = z.infer<typeof TARGET_SCHEMA>;
+
+export const SAVED_SESSION_SCHEMA = z.object({
+  name: z.string(),
+  config: RECORD_SESSION_SCHEMA,
+});
+export type SavedSessionSchema = z.infer<typeof SAVED_SESSION_SCHEMA>;
+
+// The schema for the root object that holds the whole state of the record
+// plugin.
+export const RECORD_PLUGIN_SCHEMA = z
+  .object({
+    target: TARGET_SCHEMA,
+    autoOpenTrace: z.boolean().default(true),
+    lastSession: RECORD_SESSION_SCHEMA.default({}),
+    savedSessions: z.array(SAVED_SESSION_SCHEMA).default([]),
+  })
+  .default({});
+export type RecordPluginSchema = z.infer<typeof RECORD_PLUGIN_SCHEMA>;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts
new file mode 100644
index 0000000..fdcbffa
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/target_connection_management_dialog.ts
@@ -0,0 +1,126 @@
+// 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 {TracedWebsocketTarget} from './traced_websocket_target';
+import {PreflightCheckRenderer} from '../pages/preflight_check_renderer';
+import {closeModal, showModal} from '../../../widgets/modal';
+import {Button} from '../../../widgets/button';
+import {TracedWebsocketTargetProvider} from './traced_websocket_provider';
+import {defer, Deferred} from '../../../base/deferred';
+
+/**
+ * Shows a dialog that allows to add a connection to another websocket endpoint
+ * other than the default 127.0.0.1:8037. This dialog is displayed when the user
+ * clicks on "connect new device" in the "Target Device" page.
+ */
+export async function showTracedConnectionManagementDialog(
+  provider: TracedWebsocketTargetProvider,
+): Promise<TracedWebsocketTarget | undefined> {
+  const resultPromise = defer<TracedWebsocketTarget | undefined>();
+  const key = 'TracedConnectioManagementDialog';
+  showModal({
+    key,
+    title: 'Connect to remote tracing service',
+    content: () =>
+      m(TracedConnectioManagementDialog, {provider, resultPromise}),
+  }).then(() => resultPromise.resolve(undefined));
+  const targetOrUndefined = await resultPromise;
+  closeModal(key);
+  return targetOrUndefined;
+}
+
+interface DialogAttrs {
+  provider: TracedWebsocketTargetProvider;
+  resultPromise: Deferred<TracedWebsocketTarget | undefined>;
+}
+class TracedConnectioManagementDialog implements m.ClassComponent<DialogAttrs> {
+  private target?: TracedWebsocketTarget;
+  private checks?: PreflightCheckRenderer;
+
+  view({attrs}: m.CVnode<DialogAttrs>) {
+    const provider = attrs.provider;
+    return m(
+      '.record-page',
+      m(
+        'div',
+        'Forward port 8037 with ssh from the local host to the ' +
+          'remote host where traced is running and invoke websocket_bridge.',
+      ),
+      m('br'),
+      m('code', 'ssh -L8037:remote_machine:8037 websocket_bridge'),
+      m('header', 'Connect a new target'),
+      m(
+        'div',
+        m('input', {
+          placeholder: 'remote_machine:8037',
+          onchange: (e: Event) =>
+            this.testConnection((e.target as HTMLInputElement).value ?? ''),
+        }),
+        m(Button, {
+          icon: 'add',
+          onclick: () => {
+            if (this.target !== undefined) {
+              provider.targets.set(this.target.wsUrl, this.target);
+            }
+            attrs.resultPromise.resolve(this.target);
+          },
+        }),
+      ),
+      this.checks && this.checks.renderTable(),
+      m('header', 'Manage targets'),
+      m(
+        'table',
+        ...Array.from(provider.targets.entries()).map(([wsUrl, target]) =>
+          m(
+            'tr',
+            m(
+              'td',
+              m(Button, {
+                icon: 'delete',
+                onclick: () => {
+                  target.disconnect();
+                  provider.targets.delete(wsUrl);
+                  provider.onTargetsChanged.notify();
+                },
+              }),
+            ),
+            m('td', m('code', wsUrl)),
+          ),
+        ),
+      ),
+    );
+  }
+
+  private testConnection(userInput: string) {
+    this.target && this.target.disconnect();
+    this.target = undefined;
+    this.checks = undefined;
+
+    let wsUrl: string;
+    if (userInput.match(/^ws(s?):\/\//)) {
+      wsUrl = userInput;
+    } else if (userInput.match(/^[^:/]+:\d+$/)) {
+      wsUrl = `ws://${userInput}/traced`;
+    } else if (userInput.match(/^[^:/]+$/)) {
+      wsUrl = `ws://${userInput}:8037/traced`;
+    } else {
+      return;
+    }
+
+    this.target = new TracedWebsocketTarget(wsUrl);
+    this.checks = new PreflightCheckRenderer(this.target);
+    this.checks.runPreflightChecks();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts
new file mode 100644
index 0000000..9c357e9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_provider.ts
@@ -0,0 +1,49 @@
+// 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 {EvtSource} from '../../../base/events';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {RecordingTargetProvider} from '../interfaces/recording_target_provider';
+import {showTracedConnectionManagementDialog} from './target_connection_management_dialog';
+import {TracedWebsocketTarget} from './traced_websocket_target';
+
+export class TracedWebsocketTargetProvider implements RecordingTargetProvider {
+  readonly id = 'traced_websocket';
+  readonly name = 'WebSocket';
+  readonly description =
+    'Allows to talk to the traced service UNIX socket via a WebSocket. ' +
+    'Requires launching the websocket_bridge on the host';
+  readonly icon = 'lan';
+  readonly supportedPlatforms = ['LINUX'] as const;
+  readonly onTargetsChanged = new EvtSource<void>();
+
+  readonly targets = new Map<string, TracedWebsocketTarget>();
+
+  constructor() {
+    // Add the default target.
+    const defaultWsUrl = 'ws://127.0.0.1:8037/traced';
+    this.targets.set(defaultWsUrl, new TracedWebsocketTarget(defaultWsUrl));
+  }
+
+  async listTargets(): Promise<TracedWebsocketTarget[]> {
+    return Array.from(this.targets.values());
+  }
+
+  pairNewTarget(): Promise<RecordingTarget | undefined> {
+    return showTracedConnectionManagementDialog(this);
+  }
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {}
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts
new file mode 100644
index 0000000..5fb9b5c
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target.ts
@@ -0,0 +1,135 @@
+// 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 protos from '../../../protos';
+import {errResult, okResult, Result} from '../../../base/result';
+import {RecordingTarget} from '../interfaces/recording_target';
+import {PreflightCheck} from '../interfaces/connection_check';
+import {AsyncWebsocket} from '../websocket/async_websocket';
+import {websocketInstructions} from '../websocket/websocket_utils';
+import {ConsumerIpcTracingSession} from '../tracing_protocol/consumer_ipc_tracing_session';
+import {WebSocketStream} from '../websocket/websocket_stream';
+import {TracingProtocol} from '../tracing_protocol/tracing_protocol';
+import {exists} from '../../../base/utils';
+import {AsyncLazy} from '../../../base/async_lazy';
+
+export class TracedWebsocketTarget implements RecordingTarget {
+  readonly kind = 'LIVE_RECORDING';
+  readonly platform = 'LINUX';
+  readonly transportType = 'WebSocket';
+
+  // This Consumer connection is only used to detect the connection state and
+  // to query servce state. each new tracing session creates a new instance,
+  // because consumer connections in traced are single-use.
+  private mgmtConsumer = new AsyncLazy<TracingProtocol>();
+
+  /**
+   * @param wsUrl 'ws://127.0.0.1:8037/traced'
+   */
+  constructor(readonly wsUrl: string) {}
+
+  get id(): string {
+    return this.wsUrl;
+  }
+
+  get name(): string {
+    return this.wsUrl;
+  }
+
+  get connected(): boolean {
+    return this.mgmtConsumer.value?.connected ?? false;
+  }
+
+  async *runPreflightChecks(): AsyncGenerator<PreflightCheck> {
+    const status = await this.connectIfNeeded();
+
+    yield {
+      name: 'WebSocket connection',
+      status: ((): Result<string> => {
+        if (!status.ok) return status;
+        return okResult('Connected');
+      })(),
+    };
+
+    if (!this.connected) return;
+    const svcStatus = await this.getServiceState();
+
+    yield {
+      name: 'Traced version',
+      status: ((): Result<string> => {
+        if (!svcStatus.ok) return svcStatus;
+        return okResult(svcStatus.value.tracingServiceVersion ?? 'N/A');
+      })(),
+    };
+
+    if (svcStatus === undefined) return;
+
+    yield {
+      name: 'Traced state',
+      status: ((): Result<string> => {
+        if (!svcStatus.ok) return svcStatus;
+        const tss = svcStatus.value;
+        return okResult(
+          `#producers: ${tss.producers?.length ?? 'N/A'}, ` +
+            `#datasources: ${tss.dataSources?.length ?? 'N/A'}, ` +
+            `#sessions: ${tss.numSessionsStarted ?? 'N/A'}`,
+        );
+      })(),
+    };
+  }
+
+  private async connectIfNeeded(): Promise<Result<TracingProtocol>> {
+    return this.mgmtConsumer.getOrCreate(() => this.createConsumerIpcChannel());
+  }
+
+  disconnect(): void {
+    this.mgmtConsumer.value?.close();
+    this.mgmtConsumer.reset();
+  }
+
+  async getServiceState(): Promise<Result<protos.ITracingServiceState>> {
+    const ipcStatus = await this.connectIfNeeded();
+    if (!ipcStatus.ok) return ipcStatus;
+    const consumerIpc = ipcStatus.value;
+    const req = new protos.QueryServiceStateRequest({});
+    const rpcCall = consumerIpc.invokeStreaming('QueryServiceState', req);
+    const resp = await rpcCall.promise;
+    if (!exists(resp.serviceState)) {
+      return errResult('Failed to decode QueryServiceStateResponse');
+    }
+    return okResult(resp.serviceState);
+  }
+
+  async startTracing(
+    traceConfig: protos.ITraceConfig,
+  ): Promise<Result<ConsumerIpcTracingSession>> {
+    const ipcStatus = await this.createConsumerIpcChannel();
+    if (!ipcStatus.ok) return ipcStatus;
+    const consumerIpc = ipcStatus.value;
+    const session = new ConsumerIpcTracingSession(consumerIpc, traceConfig);
+    return okResult(session);
+  }
+
+  private async createConsumerIpcChannel(): Promise<Result<TracingProtocol>> {
+    const maybeSock = await AsyncWebsocket.connect(this.wsUrl);
+    if (maybeSock == undefined) {
+      return errResult(
+        `Failed to connect ${this.wsUrl}. ${websocketInstructions()}`,
+      );
+    }
+    const stream = new WebSocketStream(maybeSock.release());
+    const consumerIpc = await TracingProtocol.create(stream);
+    return okResult(consumerIpc);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.ts
new file mode 100644
index 0000000..4f2b246
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/consumer_ipc_tracing_session.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 protos from '../../../protos';
+import {EvtSource} from '../../../base/events';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {
+  TracingSession,
+  TracingSessionLogEntry,
+  TracingSessionState,
+} from '../interfaces/tracing_session';
+import {TracingProtocol} from './tracing_protocol';
+
+/**
+ * A concrete implementation of {@link TracingSession} over a
+ * Perfetto IPC Tracing Procol. This class is suitable for all cases where we
+ * are able to obtain, in a way or another, a byte stream to talk to the traced
+ * consumer socket.
+ */
+export class ConsumerIpcTracingSession implements TracingSession {
+  private consumerIpc: TracingProtocol;
+  private _state: TracingSessionState = 'RECORDING';
+  readonly logs = new Array<TracingSessionLogEntry>();
+  private traceBuf = new ResizableArrayBuffer(64 * 1024);
+  readonly onSessionUpdate = new EvtSource<void>();
+
+  constructor(consumerIpc: TracingProtocol, traceConfig: protos.ITraceConfig) {
+    this.consumerIpc = consumerIpc;
+    this.consumerIpc.onClose = this.onProtocolClose.bind(this);
+    this.start(traceConfig);
+  }
+
+  get state(): TracingSessionState {
+    return this._state;
+  }
+
+  private async start(traceConfig: protos.ITraceConfig): Promise<void> {
+    const req = new protos.EnableTracingRequest({traceConfig});
+    this.log(`Starting trace, durationMs: ${traceConfig.durationMs}`);
+    const resp = await this.consumerIpc.invoke('EnableTracing', req);
+    this.onTraceStopped(resp.error);
+  }
+
+  async stop(): Promise<void> {
+    if (this._state !== 'RECORDING') return;
+    this.setState('STOPPING');
+    // Initiator=kPerfettoCmd, Reason=kTraceStop. See flush_flags.h.
+    const flags = (2 << 4) | 2;
+    this.log('Flushing data sources');
+    await this.consumerIpc.invoke('Flush', new protos.FlushRequest({flags}));
+    this.log('Flush complete, stopping trace');
+    const disReq = new protos.DisableTracingRequest({});
+    await this.consumerIpc.invoke('DisableTracing', disReq);
+  }
+
+  async cancel(): Promise<void> {
+    if (!['RECORDING', 'STOPPING'].includes(this._state)) return;
+    const req = new protos.FreeBuffersRequest({});
+    await this.consumerIpc.invoke('FreeBuffers', req);
+    this.fail('Trace cancelled');
+  }
+
+  async getBufferUsagePct(): Promise<number | undefined> {
+    if (this._state !== 'RECORDING') return undefined;
+    const req = new protos.GetTraceStatsRequest({});
+    const resp = await this.consumerIpc.invoke('GetTraceStats', req);
+    let totSize = 0;
+    let usedSize = 0;
+    for (const buf of resp.traceStats?.bufferStats ?? []) {
+      totSize += buf.bufferSize ?? 0;
+      // bytesWritten can be >> bufferSize for ring buffer traces.
+      usedSize += Math.min(buf.bytesWritten ?? 0, buf.bufferSize ?? 0);
+    }
+    return Math.min(Math.round((100 * usedSize) / totSize), 100);
+  }
+
+  private onTraceStopped(error: string) {
+    if (error !== '') {
+      this.fail(error);
+      return;
+    }
+    if (this.consumerIpc === undefined) {
+      return; // Spurious event after we failed.
+    }
+    // There is nothing more to do if we arrive here via cancel() or an error.
+    if (!['STOPPING', 'RECORDING'].includes(this._state)) return;
+
+    // We reach this point either:
+    // 1. In state == 'RECORDING', if the durationMs expired and the
+    //    EnableTracing request is resolved.
+    // 2. In state == 'STOPPING', if the user has pressed stop().
+    this.setState('STOPPING');
+    this.log('Tracing stopped. Reading back data');
+    const rbreq = new protos.ReadBuffersRequest({});
+    const stream = this.consumerIpc.invokeStreaming('ReadBuffers', rbreq);
+    stream.onTraceData = this.onTraceData.bind(this);
+  }
+
+  getTraceData(): Uint8Array | undefined {
+    if (this._state !== 'FINISHED') return undefined;
+    const buf = this.traceBuf.get();
+    return buf;
+  }
+
+  private onTraceData(packets: Uint8Array, hasMore: boolean) {
+    this.traceBuf.append(packets);
+    if (hasMore) return;
+
+    this.setState('FINISHED');
+    this.consumerIpc?.close();
+  }
+
+  private onProtocolClose() {
+    if (this._state === 'RECORDING') {
+      this.setState('ERRORED');
+      this.fail('Protocol disconnected');
+    }
+  }
+
+  private setState(newState: TracingSessionState) {
+    this._state = newState;
+    this.onSessionUpdate.notify();
+  }
+
+  private log(message: string, isError = false) {
+    this.logs.push({
+      message,
+      timestamp: new Date(),
+      isError,
+    });
+    this.onSessionUpdate.notify();
+  }
+
+  fail(error: string) {
+    this.log(`Tracing failed: ${error}`, /* isError */ true);
+    this.setState('ERRORED');
+    this.consumerIpc.close();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts
new file mode 100644
index 0000000..7b9d860
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/packet_assembler.ts
@@ -0,0 +1,75 @@
+// 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 protos from '../../../protos';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {exists} from '../../../base/utils';
+
+/**
+ * Utility class to re-assemble trace packets from slice fragments.
+ * This is needed to deal with ReadBuffersResponse. Each ReadBuffersResponse
+ * provies an array of slices. A slice can be == a packet, or a fragment of it.
+ * Furthermore each ReadBufferResponse can provide slices for >1 packet (or for
+ * a packet and a bit). This class deals with the reassembly.
+ */
+export class PacketAssembler {
+  // Buffers the incoming slices until we see a full packet.
+  private curPacketSlices = new Array<Uint8Array>();
+
+  /**
+   * @param rdResp a ReadBufferResponse containing an array of slices.
+   * @returns A protos.perfetto.Trace protobuf-encoded buffer containing a
+   * sequence of whole packets. This buffer is suitable to be pushed into
+   * TraceProcessor, traceconv or other perfetto tools.
+   */
+  pushSlices(rdResp: protos.IReadBuffersResponse): Uint8Array {
+    const traceBuf = new ResizableArrayBuffer(4096);
+    for (const slice of rdResp.slices ?? []) {
+      if (!exists(slice.data)) continue;
+      this.curPacketSlices.push(slice.data);
+      if (!Boolean(slice.lastSliceForPacket)) {
+        continue;
+      }
+
+      // We received all the slices for the current packet.
+      // Below we assemble all the slices for each packet together and
+      // prepend them with the proto preamble.
+      const slices = this.curPacketSlices.splice(0); // ps = std::move(this.ps).
+
+      // We receive 1+ slices per packet. The slices contain only the payload
+      // of the packet, but not the packet preamble itself. We have to write
+      // the packet proto preamble ourselves. In order to do so we need to first
+      // compute the total packet size.
+      const totLen = slices.reduce((a, buf) => a + buf.length, 0);
+
+      // Becuase the packet size is varint-encoded, we don't know how many bytes
+      // the premable is going to take. Allow for 10 bytes of preamble. We will
+      // subarray() to the actual length at the end of this function.
+      const preamble: number[] = [TRACE_PACKET_PROTO_TAG];
+      let lenVarint = totLen;
+      do {
+        preamble.push((lenVarint & 0x7f) | (lenVarint > 0x7f ? 0x80 : 0));
+        lenVarint >>>= 7;
+      } while (lenVarint > 0);
+      traceBuf.append(preamble);
+      slices.forEach((slice) => traceBuf.append(slice));
+    } // for(slices)
+    return traceBuf.get();
+  }
+}
+
+const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
+const TRACE_PACKET_PROTO_ID = 1;
+const TRACE_PACKET_PROTO_TAG =
+  (TRACE_PACKET_PROTO_ID << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts
new file mode 100644
index 0000000..33c9380
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/tracing_protocol/tracing_protocol.ts
@@ -0,0 +1,326 @@
+// 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 protobuf from 'protobufjs/minimal';
+import protos from '../../../protos';
+
+import {ByteStream} from '../interfaces/byte_stream';
+import {ProtoRingBuffer} from '../../../trace_processor/proto_ring_buffer';
+import {defer} from '../../../base/deferred';
+import {exists} from '../../../base/utils';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {PacketAssembler} from './packet_assembler';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+
+/**
+ * Implements the Consumer side of the Perfetto Tracing Protocol.
+ * https://perfetto.dev/docs/design-docs/api-and-abi#socket-protocol
+ *
+ * The passed stream must be a byte stream to the traced consumer port,
+ * e.g. obatained by connecting adb to the /dev/socket/traced_consumer.
+ */
+export class TracingProtocol {
+  private rxBuf = new ProtoRingBuffer('FIXED_SIZE');
+
+  private pendingInvokes = new Map<number, PendingInvoke>();
+
+  // Wire protocol request ID. After each request it is increased. It is needed
+  // to keep track of the type of request, and parse the response correctly.
+  // We start from 2 because the static create() method takes the first one
+  // for binding the service.
+  private requestId = 2;
+
+  onClose = () => {};
+
+  // We have a separate factory method to await the initial service binding, so
+  // we can return an object that is functional (methods can be invoked) and
+  // avoid buffering.
+  static async create(stream: ByteStream): Promise<TracingProtocol> {
+    // Send the bindService request. This is a one-off request to connect to the
+    // consumer port and list the RPC methods available.
+    const requestId = 1;
+    const txFrame = new protos.IPCFrame({
+      requestId,
+      msgBindService: new protos.IPCFrame.BindService({
+        serviceName: 'ConsumerPort',
+      }),
+    });
+    const repsponsePromise = defer<Uint8Array>();
+    const rxFrameBuf = new ProtoRingBuffer('FIXED_SIZE');
+    stream.onData = (data) => {
+      rxFrameBuf.append(data);
+      const rxFrame = rxFrameBuf.readMessage();
+      rxFrame && repsponsePromise.resolve(rxFrame);
+    };
+    TracingProtocol.sendFrame(stream, txFrame);
+
+    // Wait for the IPC reply. There is no state machine or queueing needed at
+    // this point (not just yet) because this is 1 req -> 1 reply.
+    const frameData = await repsponsePromise;
+    const rxFrame = protos.IPCFrame.decode(frameData);
+    assertTrue(rxFrame.msg === 'msgBindServiceReply');
+    const replyMsg = assertExists(rxFrame.msgBindServiceReply);
+    const boundMethods = new Map<string, number>();
+    assertTrue(replyMsg.success === true);
+    const serviceId = assertExists(replyMsg.serviceId);
+    for (const m of assertExists(replyMsg.methods)) {
+      boundMethods.set(assertExists(m.name), assertExists(m.id));
+    }
+    // Now that the details of the RPC methods are known, build and return the
+    // TracingProtocol object, so the caller can finally make calls.
+    return new TracingProtocol(stream, serviceId, boundMethods);
+  }
+
+  private constructor(
+    private stream: ByteStream,
+    private serviceId: number,
+    private boundMethods: Map<string, number>,
+  ) {
+    stream.onData = this.onStreamData.bind(this);
+    stream.onClose = () => this.close();
+  }
+
+  async invoke<T extends RpcMethodName>(
+    methodName: T,
+    req: RequestType<T>,
+  ): Promise<ResponseType<T>> {
+    const method = RPC_METHODS[methodName];
+    const resultPromise = defer<ResponseType<T>>();
+    const pendingInvoke: PendingInvoke = {
+      onResponse: (data: Uint8Array | undefined, hasMore: boolean) => {
+        assertFalse(hasMore); // Should have used invokeStreaming instead.
+        const response = exists(data)
+          ? method.respType.decode(data)
+          : method.respType.create();
+        resultPromise.resolve(response as ResponseType<T>);
+      },
+    };
+    this.beginInvoke(methodName, req, pendingInvoke);
+    return resultPromise;
+  }
+
+  invokeStreaming<T extends RpcStreamingMethodName>(
+    methodName: T,
+    req: RequestType<T>,
+  ): StreamingResponseType<T> {
+    const method = RPC_STREAMING_METHODS[methodName];
+    const streamDecoder = method.respType.createStreamingDecoder();
+
+    const pendingInvoke: PendingInvoke = {
+      onResponse: (data: Uint8Array | undefined, hasMore: boolean) => {
+        streamDecoder.decode(data, hasMore);
+      },
+    };
+    this.beginInvoke(methodName, req, pendingInvoke);
+    return streamDecoder as StreamingResponseType<T>;
+  }
+
+  // This call can arrive from two plaes:
+  // 1. The user clicking on Stop/Cancel. In this case ConsumerIpcTracingSession
+  //    calls this.consumerIpc.close().
+  // 2. Stream disconnected is detected (e.g. the user pulls the cable). In this
+  //    case we get here via stream.onClose = () => this.close().
+  close() {
+    if (this.stream.connected) {
+      this.stream.close();
+    }
+    this.pendingInvokes.clear();
+    this.onClose();
+  }
+
+  get connected() {
+    return this.stream.connected;
+  }
+
+  [Symbol.dispose]() {
+    this.close();
+  }
+
+  private beginInvoke<T extends RpcMethodName | RpcStreamingMethodName>(
+    methodName: T,
+    req: RequestType<T>,
+    pendingInvoke: PendingInvoke,
+  ): void {
+    const methodId = this.boundMethods.get(methodName);
+    if (methodId === undefined) {
+      throw new Error(`RPC Error: method ${methodName} not supported`);
+    }
+    const requestId = this.requestId++;
+    const argType =
+      methodName in RPC_METHODS
+        ? RPC_METHODS[methodName as RpcMethodName].argType
+        : RPC_STREAMING_METHODS[methodName as RpcStreamingMethodName].argType;
+    const argsProto: Uint8Array = argType.encode(req).finish();
+    const frame = new protos.IPCFrame({
+      requestId,
+      msgInvokeMethod: new protos.IPCFrame.InvokeMethod({
+        serviceId: this.serviceId,
+        methodId: methodId,
+        argsProto,
+      }),
+    });
+    TracingProtocol.sendFrame(this.stream, frame);
+    this.pendingInvokes.set(requestId, pendingInvoke);
+  }
+
+  private onStreamData(data: Uint8Array): void {
+    this.rxBuf.append(data);
+    for (;;) {
+      const frameData = this.rxBuf.readMessage();
+      if (frameData === undefined) break;
+      this.parseFrame(frameData);
+    }
+  }
+
+  private parseFrame(frameData: Uint8Array): void {
+    // Get a copy of the ArrayBuffer to avoid the original being overriden.
+    // See 170256902#comment21
+    const frame = protos.IPCFrame.decode(frameData.slice());
+    if (frame.msg === 'msgInvokeMethodReply') {
+      const reply = assertExists(frame.msgInvokeMethodReply);
+      // We process messages without a `replyProto` field (for instance
+      // `FreeBuffers` does not have `replyProto`). However, we ignore messages
+      // without a valid 'success' field.
+      assertTrue(Boolean(reply.success));
+
+      const pendInvoke = assertExists(this.pendingInvokes.get(frame.requestId));
+      pendInvoke.onResponse(
+        reply.replyProto ?? undefined,
+        Boolean(reply.hasMore),
+      );
+      if (!reply.hasMore) {
+        this.pendingInvokes.delete(frame.requestId);
+      }
+    } else {
+      throw new Error(`Tracing protocol: unrecognized frame ${frame.msg}`);
+    }
+  }
+
+  private static sendFrame(
+    stream: ByteStream,
+    frame: protos.IPCFrame,
+  ): Promise<void> {
+    const writer = protobuf.Writer.create();
+    writer.fixed32(0); // Reserve space for the 4 bytes header (frame len).
+    const frameData = protos.IPCFrame.encode(frame, writer).finish().slice();
+    const frameLen = frameData.length - 4;
+    const dv = new DataView(frameData.buffer);
+    dv.setUint32(0, frameLen, /* littleEndian */ true); // Write the header.
+    return stream.write(frameData);
+  }
+}
+
+export class PacketStream {
+  static createStreamingDecoder(): PacketStream {
+    return new PacketStream();
+  }
+
+  private traceBuf = new PacketAssembler();
+
+  onTraceData: (packets: Uint8Array, hasMore: boolean) => void = () => {};
+
+  decode(data: Uint8Array | undefined, hasMore: boolean) {
+    if (data === undefined) {
+      this.onTraceData(new Uint8Array(), hasMore);
+      return;
+    }
+
+    // ReadBuffers returns 1+ slices. They can form 1 packet (usually),
+    // >1 packet, or a fraction of a packet.
+    const rdresp = protos.ReadBuffersResponse.decode(data);
+    const packets: Uint8Array = this.traceBuf.pushSlices(rdresp);
+    this.onTraceData(packets, hasMore);
+  }
+}
+
+// QueryServiceStateResponse can be split in several chunks if the service state
+// exceeds the 128KB ipc limit. This class simply merges them and exposes the
+// merged result once hasMore = false.
+class ServiceStateMerger {
+  static createStreamingDecoder(): ServiceStateMerger {
+    return new ServiceStateMerger();
+  }
+
+  private rxBuf = new ResizableArrayBuffer();
+  readonly promise = defer<protos.QueryServiceStateResponse>();
+
+  decode(data: Uint8Array | undefined, hasMore: boolean) {
+    if (data !== undefined) {
+      this.rxBuf.append(data);
+    }
+
+    if (!hasMore) {
+      const msg = protos.QueryServiceStateResponse.decode(this.rxBuf.get());
+      this.rxBuf.clear();
+      this.promise.resolve(msg);
+    }
+  }
+}
+
+const RPC_METHODS = {
+  EnableTracing: {
+    argType: protos.EnableTracingRequest,
+    respType: protos.EnableTracingResponse,
+  },
+  DisableTracing: {
+    argType: protos.DisableTracingRequest,
+    respType: protos.DisableTracingResponse,
+  },
+  Flush: {
+    argType: protos.FlushRequest,
+    respType: protos.FlushResponse,
+  },
+  FreeBuffers: {
+    argType: protos.FreeBuffersRequest,
+    respType: protos.FreeBuffersResponse,
+  },
+  GetTraceStats: {
+    argType: protos.GetTraceStatsRequest,
+    respType: protos.GetTraceStatsResponse,
+  },
+};
+
+const RPC_STREAMING_METHODS = {
+  ReadBuffers: {
+    argType: protos.ReadBuffersRequest,
+    respType: PacketStream,
+  },
+  QueryServiceState: {
+    argType: protos.QueryServiceStateRequest,
+    respType: ServiceStateMerger,
+  },
+};
+
+type RpcMethods = typeof RPC_METHODS;
+type RpcStreamingMethods = typeof RPC_STREAMING_METHODS;
+
+export type RpcMethodName = keyof RpcMethods & string;
+export type RpcStreamingMethodName = keyof RpcStreamingMethods & string;
+export type RpcAllMethodName = RpcMethodName | RpcStreamingMethodName;
+
+type RequestType<T extends RpcAllMethodName> = InstanceType<
+  (RpcMethods & RpcStreamingMethods)[T]['argType']
+>;
+
+type ResponseType<T extends RpcMethodName> = InstanceType<
+  RpcMethods[T]['respType']
+>;
+
+type StreamingResponseType<T extends RpcStreamingMethodName> = InstanceType<
+  RpcStreamingMethods[T]['respType']
+>;
+
+interface PendingInvoke {
+  onResponse: (data: Uint8Array | undefined, hasMore: boolean) => void;
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts
new file mode 100644
index 0000000..c871191
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/async_websocket.ts
@@ -0,0 +1,143 @@
+// 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 {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {ResizableArrayBuffer} from '../../../base/resizable_array_buffer';
+import {utf8Decode} from '../../../base/string_utils';
+
+/**
+ * A wrapper around WebSocket with async methods.
+ * It allows nicer usage with await, allowing to write async sequential code.
+ * E.g.:
+ * const sock = await AsyncWebsocket.connect('ws://...');
+ * sock.send('command');
+ * const response = await sock.waitForData(42);  // Wait to receive 42 bytes.
+ * sock.send('command2');
+ * const response2 = await sock.waitForData(10);  // Wait to receive 10 bytes.
+ */
+export class AsyncWebsocket {
+  private sock?: WebSocket;
+  private rxBuf = new ResizableArrayBuffer(128);
+  private rxBufRead = 0;
+  private rxPromise?: Deferred<Uint8Array>;
+  private rxPromiseBytes = 0;
+
+  static async connect(url: string): Promise<AsyncWebsocket | undefined> {
+    const sock = new WebSocket(url);
+    sock.binaryType = 'arraybuffer';
+
+    // In case of a connection failure, there are two possible scenarios:
+    // 1. The failure is immediate (e.g. due to CSP blocking access). In this
+    //    case the onclose() is NOT triggered because happens before we get a
+    //    chance to register the handler.
+    // 2. The connection failure happens in the near future. In this case we
+    //    infer a connection failure by observing on onclose().
+    const readyState = sock.readyState;
+    if (readyState === WebSocket.CLOSED || readyState === WebSocket.CLOSED) {
+      return undefined; // Case 1.
+    }
+    const connectPromise = defer<AsyncWebsocket | undefined>();
+    const resolveConnectPromise = (success: boolean) => {
+      sock.onclose = null;
+      sock.onopen = null;
+      if (success) {
+        connectPromise.resolve(new AsyncWebsocket(sock));
+      } else {
+        connectPromise.resolve(undefined);
+      }
+    };
+    sock.onopen = () => resolveConnectPromise(true);
+    sock.onclose = () => resolveConnectPromise(false);
+    return connectPromise;
+  }
+
+  private constructor(sock: WebSocket) {
+    this.sock = sock;
+    sock.onmessage = this.onSocketMessage.bind(this);
+  }
+
+  /** Turns this back into a standard WebSocket. */
+  release(): WebSocket {
+    const sock = assertExists(this.sock);
+    this.sock = undefined;
+    sock.onmessage = null;
+    sock.onopen = null;
+    sock.onclose = null;
+    sock.onerror = null;
+    return sock;
+  }
+
+  send(data: string | ArrayBufferLike) {
+    assertExists(this.sock).send(data);
+  }
+
+  waitForData(numBytes: number): Promise<Uint8Array> {
+    if (this.rxPromise !== undefined) {
+      throw new Error('Another unresolved waitForData() is pending already');
+    }
+    const rxPromise = defer<Uint8Array>();
+    if (numBytes === 0) {
+      rxPromise.resolve(new Uint8Array());
+      return rxPromise;
+    }
+    this.rxPromise = rxPromise;
+    this.rxPromiseBytes = numBytes;
+    this.resolveRxPromiseIfEnoughDataAvail();
+    return rxPromise;
+  }
+
+  close() {
+    this.sock?.close();
+  }
+
+  get connected(): boolean {
+    return this.sock?.readyState === WebSocket.OPEN;
+  }
+
+  [Symbol.dispose]() {
+    this.close();
+  }
+
+  async waitForString(numBytes: number): Promise<string> {
+    const data = await this.waitForData(numBytes);
+    assertTrue(data.length === numBytes);
+    return utf8Decode(data);
+  }
+
+  private async onSocketMessage(e: MessageEvent) {
+    assertTrue(e.data instanceof ArrayBuffer);
+    const buf = new Uint8Array(e.data as ArrayBuffer);
+    this.rxBuf.append(buf);
+    this.resolveRxPromiseIfEnoughDataAvail();
+  }
+
+  private resolveRxPromiseIfEnoughDataAvail() {
+    if (this.rxPromise === undefined) return; // Nobody is waiting any data.
+    assertTrue(this.rxPromiseBytes > 0);
+    const bytesWanted = this.rxPromiseBytes;
+    const bytesAvail = this.rxBuf.size - this.rxBufRead;
+    if (bytesWanted > bytesAvail) return; // Not enough data.
+    const buf = this.rxBuf
+      .get()
+      .slice(this.rxBufRead, this.rxBufRead + this.rxPromiseBytes);
+    assertTrue(buf.length === bytesWanted);
+    this.rxBufRead += bytesWanted;
+
+    const rxPromise = this.rxPromise;
+    this.rxPromise = undefined;
+    this.rxPromiseBytes = 0;
+    rxPromise.resolve(buf);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts
new file mode 100644
index 0000000..fd544e1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_stream.ts
@@ -0,0 +1,40 @@
+// 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 {assertTrue} from '../../../base/logging';
+import {ByteStream} from '../interfaces/byte_stream';
+
+export class WebSocketStream extends ByteStream {
+  constructor(private sock: WebSocket) {
+    super();
+    sock.binaryType = 'arraybuffer';
+    sock.onclose = () => this.onClose();
+    sock.onmessage = async (e: MessageEvent) => {
+      assertTrue(e.data instanceof ArrayBuffer);
+      this.onData(new Uint8Array(e.data as ArrayBuffer));
+    };
+  }
+
+  get connected(): boolean {
+    return this.sock.readyState === WebSocket.OPEN;
+  }
+
+  async write(data: string | Uint8Array): Promise<void> {
+    this.sock.send(data);
+  }
+
+  close(): void {
+    this.sock.close();
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts
new file mode 100644
index 0000000..0246092
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTraceV2/websocket/websocket_utils.ts
@@ -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.
+
+// The messages read by the adb server have their length prepended in hex.
+// This method adds the length at the beginning of the message.
+// Example: 'host:track-devices' -> '0012host:track-devices'
+// go/codesearch/aosp-android11/system/core/adb/SERVICES.TXT
+export function prefixWithHexLen(cmd: string) {
+  const hdr = cmd.length.toString(16).padStart(4, '0');
+  return hdr + cmd;
+}
+
+export function websocketInstructions(os?: 'ANDROID') {
+  return (
+    'Instructions:\n' +
+    (os === 'ANDROID' ? 'adb start-server\n' : '') +
+    'curl -LO https://get.perfetto.dev/tracebox\n' +
+    'chmod +x ./tracebox\n' +
+    './tracebox websocket_bridge\n'
+  );
+}
+
+export function disposeWebsocket(ws: WebSocket) {
+  ws.onclose = null;
+  ws.onerror = null;
+  ws.onmessage = null;
+  ws.onopen = null;
+  try {
+    ws.close();
+  } catch {}
+}
diff --git a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
index f2af628..601517c 100644
--- a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
@@ -40,7 +40,7 @@
   getTrackShellButtons(): m.Children {
     return m(Button, {
       onclick: () => {
-        this.trace.workspace.findTrackByUri(this.uri)?.remove();
+        this.trace.workspace.getTrackByUri(this.uri)?.remove();
       },
       icon: Icons.Close,
       title: 'Close',
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
index 7154cd6..a4bf549 100644
--- a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
@@ -101,7 +101,7 @@
 export interface SqlColumn {
   readonly name: string;
   readonly description: string;
-  readonly type: string;
+  readonly type: SqlType;
 
   // Translates this column to SimpleColumn.
   asSimpleColumn(tableName: string): SimpleColumn;
@@ -114,3 +114,10 @@
   readonly description: string;
   readonly type: string;
 }
+
+export interface SqlType {
+  readonly name: string;
+  readonly shortName: string;
+  readonly table: string | undefined;
+  readonly column: string | undefined;
+}
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
index 6b9739b..8796665 100644
--- a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
@@ -23,6 +23,7 @@
   SqlPackage,
   SqlTable,
   SqlTableFunction,
+  SqlType,
 } from './sql_modules';
 import {SqlTableDescription} from '../../components/widgets/sql/legacy_table/table_description';
 import {
@@ -219,57 +220,62 @@
 
 class StdlibColumnImpl implements SqlColumn {
   name: string;
-  type: string;
+  type: SqlType;
   description: string;
 
   constructor(docs: DocsArgOrColSchemaType) {
-    this.type = docs.type;
+    this.type = {
+      name: docs.type,
+      shortName: docs.name.split('(')[0],
+      table: docs.table ? docs.table : undefined,
+      column: docs.column ? docs.column : undefined,
+    };
     this.description = docs.desc;
     this.name = docs.name;
   }
 
   asSimpleColumn(tableName: string): SimpleColumn {
-    if (this.type === 'TIMESTAMP') {
+    if (this.type.shortName === 'TIMESTAMP') {
       return createTimestampColumn(this.name);
     }
-    if (this.type === 'DURATION') {
+    if (this.type.shortName === 'DURATION') {
       return createDurationColumn(this.name);
     }
 
-    if (this.name === 'ID') {
-      if (tableName === 'slice') {
-        return createSliceIdColumn(this.name);
-      }
-      if (tableName === 'thread') {
-        return createThreadIdColumn(this.name);
-      }
-      if (tableName === 'process') {
-        return createProcessIdColumn(this.name);
-      }
-      if (tableName === 'thread_state') {
-        return createThreadStateIdColumn(this.name);
-      }
-      if (tableName === 'sched') {
-        return createSchedIdColumn(this.name);
+    if (this.type.shortName === 'ID') {
+      switch (tableName.toLowerCase()) {
+        case 'slice':
+          return createSliceIdColumn(this.name);
+        case 'thread':
+          return createThreadIdColumn(this.name);
+        case 'process':
+          return createProcessIdColumn(this.name);
+        case 'thread_state':
+          return createThreadStateIdColumn(this.name);
+        case 'sched':
+          return createSchedIdColumn(this.name);
       }
       return createStandardColumn(this.name);
     }
 
-    if (this.type === 'JOINID(slice.id)') {
-      return createSliceIdColumn(this.name);
+    if (this.type.shortName === 'JOINID') {
+      if (this.type.table === undefined) {
+        return createStandardColumn(this.name);
+      }
+      switch (this.type.table.toLowerCase()) {
+        case 'slice':
+          return createSliceIdColumn(this.name);
+        case 'thread':
+          return createThreadIdColumn(this.name);
+        case 'process':
+          return createProcessIdColumn(this.name);
+        case 'thread_state':
+          return createThreadStateIdColumn(this.name);
+        case 'sched':
+          return createSchedIdColumn(this.name);
+      }
     }
-    if (this.type === 'JOINID(thread.id)') {
-      return createThreadIdColumn(this.name);
-    }
-    if (this.type === 'JOINID(process.id)') {
-      return createProcessIdColumn(this.name);
-    }
-    if (this.type === 'JOINID(thread_state.id)') {
-      return createThreadStateIdColumn(this.name);
-    }
-    if (this.type === 'JOINID(sched.id)') {
-      return createSchedIdColumn(this.name);
-    }
+
     return createStandardColumn(this.name);
   }
 }
@@ -290,6 +296,8 @@
   name: z.string(),
   type: z.string(),
   desc: z.string(),
+  table: z.string().nullable(),
+  column: z.string().nullable(),
 });
 type DocsArgOrColSchemaType = z.infer<typeof ARG_OR_COL_SCHEMA>;
 
diff --git a/ui/src/plugins/dev.perfetto.StandardGroups/index.ts b/ui/src/plugins/dev.perfetto.StandardGroups/index.ts
new file mode 100644
index 0000000..ae684a7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.StandardGroups/index.ts
@@ -0,0 +1,104 @@
+// 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 {PerfettoPlugin} from '../../public/plugin';
+import {TrackNode, Workspace} from '../../public/workspace';
+
+// Type indicating the standard groups supported by this plugin.
+export type StandardGroup =
+  | 'USER_INTERACTION'
+  | 'THERMALS'
+  | 'POWER'
+  | 'IO'
+  | 'MEMORY'
+  | 'HARDWARE'
+  | 'CPU'
+  | 'GPU'
+  | 'NETWORK'
+  | 'SYSTEM';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.StandardGroups';
+
+  async onTraceLoad() {}
+
+  /**
+   * Gets or creates a standard group to place tracks into.
+   *
+   * @param workspace - The workspace on which to create the group.
+   */
+  getOrCreateStandardGroup(
+    workspace: Workspace,
+    group: StandardGroup,
+  ): TrackNode {
+    switch (group) {
+      case 'USER_INTERACTION':
+        // Expand this by default
+        return getOrCreateGroup(
+          workspace,
+          '/standard_group_user_interaction',
+          'User Interaction',
+          false,
+        );
+      case 'THERMALS':
+        return getOrCreateGroup(
+          workspace,
+          '/standard_group_thermal',
+          'Thermals',
+        );
+      case 'POWER':
+        return getOrCreateGroup(workspace, '/standard_group_power', 'Power');
+      case 'CPU':
+        return getOrCreateGroup(workspace, '/standard_group_cpu', 'CPU');
+      case 'GPU':
+        return getOrCreateGroup(workspace, '/standard_group_gpu', 'GPU');
+      case 'HARDWARE':
+        return getOrCreateGroup(
+          workspace,
+          '/standard_group_hardware',
+          'Hardware',
+        );
+      case 'IO':
+        return getOrCreateGroup(workspace, '/standard_group_io', 'IO');
+      case 'MEMORY':
+        return getOrCreateGroup(workspace, '/standard_group_memory', 'Memory');
+      case 'NETWORK':
+        return getOrCreateGroup(
+          workspace,
+          '/standard_group_network',
+          'Network',
+        );
+      case 'SYSTEM':
+        return getOrCreateGroup(workspace, '/standard_group_system', 'System');
+    }
+  }
+}
+
+// Internal utility function to avoid duplicating the logic to get or create a
+// group by ID.
+function getOrCreateGroup(
+  workspace: Workspace,
+  id: string,
+  title: string,
+  collapsed: boolean = true,
+): TrackNode {
+  const group = workspace.getTrackById(id);
+  if (group) {
+    return group;
+  } else {
+    const group = new TrackNode({id, title, isSummary: true, collapsed});
+    workspace.addChildInOrder(group);
+    return group;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS b/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS
new file mode 100644
index 0000000..9f37949
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SysUIWorkspace/OWNERS
@@ -0,0 +1 @@
+nicomazz@google.com
diff --git a/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts b/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts
new file mode 100644
index 0000000..8a5b1d7
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SysUIWorkspace/index.ts
@@ -0,0 +1,263 @@
+// Copyright (C) 2025 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 {NUM, STR} from '../../trace_processor/query_result';
+import {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import {TrackNode, Workspace} from '../../public/workspace';
+
+const TRACKS_TO_COPY: string[] = [
+  'L<',
+  'UI Events',
+  'IKeyguardService',
+  'Transition:',
+];
+const SYSTEM_UI_PROCESS: string = 'com.android.systemui';
+
+// Plugin that creates an opinionated Workspace specific for SysUI
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.SysUIWorkspace';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.SysUIWorkspace#CreateSysUIWorkspace',
+      name: 'Create System UI workspace',
+      callback: () =>
+        ProcessWorkspaceFactory.create(
+          ctx,
+          SYSTEM_UI_PROCESS,
+          'System UI',
+          TRACKS_TO_COPY,
+        ),
+    });
+  }
+}
+
+/**
+ *  Creates a workspace for a process with the following tracks:
+ *  - timelines
+ *  - main thread and render thread
+ *  - All other ui threads in a group
+ *  - List of tracks having name manually provided to this class constructor
+ *  - groups tracks having the "/(?<groupName>.*)##(?<trackName>.*)/" format
+ *    (e.g. "notifications##visible" will create a "visible" track inside the
+ *    "notification" group)
+ *
+ *  This is useful to reduce the clutter when focusing on a single process, and
+ *  organizing tracks related to the same area in groups.
+ */
+class ProcessWorkspaceFactory {
+  private readonly ws: Workspace;
+  private readonly processTracks: TrackNode[];
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly process: ProcessIdentifier,
+    private readonly workspaceName: string,
+    private readonly topLevelTracksToPin: string[] = [],
+  ) {
+    // We're going to iterate them often: let's filter the process ones.
+    this.processTracks = this.findProcessTracks();
+    this.ws = this.trace.workspaces.createEmptyWorkspace(this.workspaceName);
+  }
+
+  /**
+   * Creates a new workspace for a specific process in a trace.
+   *
+   * No workspace is created if it was there already.
+   * This is expected to be called from the default workspace.
+   *
+   * @param trace
+   * @param packageName Name of the Android package to create the workspace for.
+   * @param workspaceName Desired name for the new workspace.
+   * @param tracksToCopy - An optional list of track names to be added to
+   *                              the new workspace
+   * @returns A `Promise` that resolves when the workspace has been created.
+   */
+  public static async create(
+    trace: Trace,
+    packageName: string,
+    workspaceName: string,
+    tracksToCopy: string[] = [],
+  ) {
+    const exists = trace.workspaces.all.find(
+      (ws) => ws.title === workspaceName,
+    );
+    if (exists) return;
+
+    const process = await getProcessInfo(trace, packageName);
+    if (!process) return;
+    const factory = new ProcessWorkspaceFactory(
+      trace,
+      process,
+      workspaceName,
+      tracksToCopy,
+    );
+    await factory.createWorkspace();
+  }
+
+  private async createWorkspace() {
+    this.pinTracksContaining('Actual Timeline', 'Expected Timeline');
+    this.pinMainThread();
+    this.pinFirstRenderThread();
+    await this.pinUiThreads();
+    this.topLevelTracksToPin.forEach((s) =>
+      this.pinTracksContainingInGroupIfNeeded(s),
+    );
+    this.createGroups();
+    this.trace.workspaces.switchWorkspace(this.ws);
+  }
+
+  private findProcessTracks(): TrackNode[] {
+    return this.trace.workspace.flatTracks.filter((track) => {
+      if (!track.uri) return false;
+      const descriptor = this.trace.tracks.getTrack(track.uri);
+      return descriptor?.tags?.upid === this.process.upid;
+    });
+  }
+
+  private pinTracksContaining(...args: string[]) {
+    args.forEach((s) => this.pinTrackContaining(s));
+  }
+
+  private pinTrackContaining(titleSubstring: string) {
+    this.getTracksContaining(titleSubstring).forEach((track) =>
+      this.ws.addChildLast(track.clone()),
+    );
+  }
+
+  private pinTracksContainingInGroupIfNeeded(
+    titleSubstring: string,
+    minSizeToGroup: number = 2,
+  ) {
+    const tracks = this.getTracksContaining(titleSubstring);
+    if (tracks.length == 0) return;
+    if (tracks.length >= minSizeToGroup) {
+      const newGroup = new TrackNode({title: titleSubstring, isSummary: true});
+      this.ws.addChildLast(newGroup);
+      tracks.forEach((track) => newGroup.addChildLast(track.clone()));
+    } else {
+      tracks.forEach((track) => this.ws.addChildLast(track.clone()));
+    }
+  }
+
+  private getTracksContaining(titleSubstring: string): TrackNode[] {
+    return this.processTracks.filter((track) =>
+      track.title.includes(titleSubstring),
+    );
+  }
+
+  private pinMainThread() {
+    const tracks = this.processTracks.filter((track) => {
+      return this.getTrackUtid(track) == this.process.upid;
+    });
+    tracks.forEach((track) => this.ws.addChildLast(track.clone()));
+  }
+
+  // In traces there might be many short-lived threads called "render thread"
+  // used to allocate stuff. We don't care about them, but only of the first one
+  // (that has lower thread id)
+  private pinFirstRenderThread() {
+    const tracks = this.getTracksContaining('RenderThread');
+    const utids = tracks
+      .map((t) => this.getTrackUtid(t))
+      .filter((utid): utid is number => utid !== undefined);
+    const minUtid = Math.min(...utids);
+
+    const toPin = tracks.filter((track) => this.getTrackUtid(track) == minUtid);
+    toPin.forEach((track) => this.ws.addChildLast(track.clone()));
+  }
+
+  private async pinUiThreads() {
+    const result = await this.trace.engine.query(`
+      INCLUDE PERFETTO MODULE slices.slices;
+      SELECT DISTINCT utid FROM _slice_with_thread_and_process_info
+      WHERE upid = ${this.process.upid}
+       AND upid != utid -- main thread excluded
+       AND name GLOB "Choreographer#doFrame*"
+    `);
+    if (result.numRows() === 0) {
+      return;
+    }
+    const uiThreadUtidsSet = new Set<number>();
+    const it = result.iter({utid: NUM});
+    for (; it.valid(); it.next()) {
+      uiThreadUtidsSet.add(it.utid);
+    }
+
+    const toPin = this.processTracks.filter((track) => {
+      const utid = this.getTrackUtid(track);
+      return utid != undefined && uiThreadUtidsSet.has(utid);
+    });
+    toPin.sort((a, b) => {
+      return a.title.localeCompare(b.title);
+    });
+    const uiThreadTrack = new TrackNode({title: 'UI Threads', isSummary: true});
+    this.ws.addChildLast(uiThreadTrack);
+    toPin.forEach((track) => uiThreadTrack.addChildLast(track.clone()));
+  }
+
+  private getTrackUtid(node: TrackNode): number | undefined {
+    return this.trace.tracks.getTrack(node.uri!)?.tags?.utid;
+  }
+
+  private createGroups() {
+    const groupRegex = /(?<groupName>.*)##(?<trackName>.*)/;
+    const trackGroups = new Map<string, TrackNode>();
+
+    this.processTracks.forEach((track) => {
+      const match = track.title.match(groupRegex);
+      if (!match?.groups) return;
+
+      const {groupName, trackName} = match.groups;
+
+      const newTrack = track.clone();
+      newTrack.title = trackName;
+
+      if (!trackGroups.has(groupName)) {
+        const newGroup = new TrackNode({title: groupName, isSummary: true});
+        this.ws.addChildLast(newGroup);
+        trackGroups.set(groupName, newGroup);
+      }
+      trackGroups.get(groupName)!.addChildLast(newTrack);
+    });
+  }
+}
+
+type ProcessIdentifier = {
+  upid: number;
+  name: string;
+};
+
+async function getProcessInfo(
+  ctx: Trace,
+  processName: string,
+): Promise<ProcessIdentifier | undefined> {
+  const result = await ctx.engine.query(`
+      INCLUDE PERFETTO MODULE android.process_metadata;
+      select
+        _process_available_info_summary.upid,
+        process.name
+      from _process_available_info_summary
+      join process using(upid)
+      where process.name = '${processName}';
+    `);
+  if (result.numRows() === 0) {
+    return undefined;
+  }
+  return result.firstRow({
+    upid: NUM,
+    name: STR,
+  });
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
index b220405..33c73e7 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_selection_aggregator.ts
@@ -23,15 +23,24 @@
   STR_NULL,
 } from '../../trace_processor/query_result';
 import {AreaSelectionAggregator} from '../../public/selection';
-import {UnionDataset} from '../../trace_processor/dataset';
+import {Dataset} from '../../trace_processor/dataset';
 import {translateState} from '../../components/sql_utils/thread_state';
-import {TrackDescriptor} from '../../public/track';
 
 export class ThreadStateSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'thread_state_aggregation';
 
-  async createAggregateView(engine: Engine, area: AreaSelection) {
-    const dataset = this.getDatasetFromTracks(area.tracks);
+  readonly schema = {
+    dur: LONG,
+    io_wait: NUM_NULL,
+    state: STR,
+    utid: NUM,
+  } as const;
+
+  async createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ) {
     if (dataset === undefined) return false;
 
     await engine.query(`
@@ -60,8 +69,8 @@
   async getExtra(
     engine: Engine,
     area: AreaSelection,
+    dataset?: Dataset,
   ): Promise<ThreadStateExtra | void> {
-    const dataset = this.getDatasetFromTracks(area.tracks);
     if (dataset === undefined) return;
 
     const query = `
@@ -164,24 +173,4 @@
   getDefaultSorting(): Sorting {
     return {column: 'total_dur', direction: 'DESC'};
   }
-
-  // Creates an optimized dataset containing the thread state events within a
-  // given list of tracks, or returns undefined if no compatible tracks are
-  // present in the list.
-  private getDatasetFromTracks(tracks: ReadonlyArray<TrackDescriptor>) {
-    const desiredSchema = {
-      dur: LONG,
-      io_wait: NUM_NULL,
-      state: STR,
-      utid: NUM,
-    };
-    const validDatasets = tracks
-      .map((track) => track.track.getDataset?.())
-      .filter((ds) => ds !== undefined)
-      .filter((ds) => ds.implements(desiredSchema));
-    if (validDatasets.length === 0) {
-      return undefined;
-    }
-    return new UnionDataset(validDatasets).optimize();
-  }
 }
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index 37206a3..e415816 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -272,7 +272,7 @@
   private onmessage(msg: MessageEvent) {
     if (this._ctx === undefined) return; // Trace unloaded
     if (!('perfettoSync' in msg.data)) return;
-    this._ctx.scheduleFullRedraw('force');
+    this._ctx.raf.scheduleFullRedraw();
     const msgData = msg.data as SyncMessage;
     const sync = msgData.perfettoSync;
     switch (sync.cmd) {
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 3e12b1c..4fed517 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -17,9 +17,12 @@
 import {PerfettoPlugin} from '../../public/plugin';
 import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
 import {TrackNode} from '../../public/workspace';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.TraceMetadata';
+  static readonly dependencies = [StandardGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const res = await ctx.engine.query(`
       select count() as cnt from (select 1 from clock_snapshot limit 1)
@@ -46,6 +49,9 @@
       track,
     });
     const trackNode = new TrackNode({uri, title});
-    ctx.workspace.addChildInOrder(trackNode);
+    const group = ctx.plugins
+      .getPlugin(StandardGroupsPlugin)
+      .getOrCreateStandardGroup(ctx.workspace, 'SYSTEM');
+    group.addChildInOrder(trackNode);
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
similarity index 88%
rename from ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
index 97de057..6d06231 100644
--- a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_details_panel.ts
@@ -140,18 +140,39 @@
   rootTable: string,
 ): Promise<CounterDetails> {
   const query = `
-    WITH CTE AS (
+    WITH CURRENT AS (
       SELECT
         id,
-        ts as leftTs,
+        ts,
         value,
-        LAG(value) OVER (ORDER BY ts) AS prevValue,
-        LEAD(ts) OVER (ORDER BY ts) AS rightTs,
-        arg_set_id AS argSetId
+        arg_set_id
       FROM ${rootTable}
-      WHERE track_id = ${trackId}
+      WHERE track_id = ${trackId} and id = ${id}
+    ),
+    PREV as (
+      SELECT
+        value
+      FROM ${rootTable}
+      WHERE track_id = ${trackId} AND ts < (select ts from CURRENT)
+      ORDER BY ts DESC
+      LIMIT 1
+    ),
+    NEXT as (
+      SELECT
+        ts
+      FROM ${rootTable}
+      WHERE track_id = ${trackId} AND ts > (select ts from CURRENT)
+      ORDER BY ts ASC
+      LIMIT 1
     )
-    SELECT * FROM CTE WHERE id = ${id}
+    SELECT
+      id,
+      ts as leftTs,
+      value,
+      arg_set_id as argSetId,
+      (SELECT value FROM PREV) as prevValue,
+      (SELECT ts FROM NEXT) as rightTs
+    FROM CURRENT
   `;
 
   const counter = await engine.query(query);
diff --git a/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
similarity index 68%
rename from ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
index 6a2bd3f..d098821 100644
--- a/ui/src/plugins/dev.perfetto.Counter/counter_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_selection_aggregator.ts
@@ -33,32 +33,43 @@
     const duration = area.end - area.start;
     const durationSec = Duration.toSeconds(duration);
 
+    await engine.query(`include perfetto module counters.intervals`);
+
     // 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 OR REPLACE PERFETTO TABLE ${this.id} 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})
+      WITH
+        res AS (
+          select c.*
+          from counter_leading_intervals!((
+            SELECT counter.*
+            FROM counter
+            WHERE counter.track_id = ${trackIds[0]}
+              AND counter.ts <= ${area.end}
+          )) c
+          WHERE c.ts + c.dur >= ${area.start}
+        ),
+        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 counter 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 counter 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 res
+        )
       SELECT
         (SELECT name FROM counter_track WHERE id = ${trackIds[0]}) AS name,
         *,
@@ -68,23 +79,31 @@
     } else {
       // Slower, but general purspose query that can aggregate multiple tracks
       query = `CREATE OR REPLACE PERFETTO TABLE ${this.id} 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
-      )
+      WITH
+        res AS (
+          select c.*
+          from counter_leading_intervals!((
+            SELECT counter.*
+            FROM counter
+            WHERE counter.track_id in (${trackIds})
+              AND counter.ts <= ${area.end}
+          )) c
+          where c.ts + c.dur >= ${area.start}
+        ),
+        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 res
+          GROUP BY track_id
+        )
       SELECT
         name,
         count,
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts
new file mode 100644
index 0000000..6bdfe65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/counter_tracks.ts
@@ -0,0 +1,426 @@
+// Copyright (C) 2025 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 {CounterOptions} from '../../components/tracks/base_counter_track';
+import {TopLevelTrackGroup, TrackGroupSchema} from './types';
+
+type CounterMode = CounterOptions['yMode'];
+
+interface CounterTrackTypeSchema {
+  type: string;
+  topLevelGroup: TopLevelTrackGroup;
+  group: string | TrackGroupSchema | undefined;
+  shareYAxis?: true;
+  mode?: CounterMode;
+}
+
+export const COUNTER_TRACK_SCHEMAS: ReadonlyArray<CounterTrackTypeSchema> = [
+  {
+    type: 'acpm_cooling_device_counter',
+    topLevelGroup: 'THERMALS',
+    group: 'ACPM Cooling Devices',
+  },
+  {
+    type: 'acpm_thermal_temperature',
+    topLevelGroup: 'THERMALS',
+    group: 'ACPM Temperature',
+  },
+  {
+    type: 'android_energy_estimation_breakdown_per_uid',
+    topLevelGroup: 'POWER',
+    group: 'Android Energy Estimates (per uid)',
+  },
+  {
+    type: 'android_energy_estimation_breakdown',
+    topLevelGroup: 'POWER',
+    group: 'Android Energy Estimates',
+  },
+  {
+    type: 'atrace_counter',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'battery_counter',
+    topLevelGroup: 'POWER',
+    group: 'Battery Counters',
+  },
+  {
+    type: 'battery_stats',
+    topLevelGroup: 'POWER',
+    group: 'Battery Stats',
+  },
+  {
+    type: 'bcl_irq',
+    topLevelGroup: undefined,
+    group: 'BCL IRQ',
+  },
+  {
+    type: 'block_io',
+    topLevelGroup: 'IO',
+    group: 'Block IO',
+  },
+  {
+    type: 'buddyinfo',
+    topLevelGroup: 'MEMORY',
+    group: 'Buddyinfo',
+  },
+  {
+    type: 'chrome_process_stats',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'clock_frequency',
+    topLevelGroup: 'HARDWARE',
+    group: 'Clock Frequency',
+  },
+  {
+    type: 'clock_state',
+    topLevelGroup: 'HARDWARE',
+    group: 'Clock State',
+  },
+  {
+    type: 'cooling_device_counter',
+    topLevelGroup: 'THERMALS',
+    group: 'Cooling Devices',
+  },
+  {
+    type: 'cpu_capacity',
+    topLevelGroup: 'CPU',
+    group: 'CPU Capacity',
+  },
+  {
+    type: 'cpu_frequency_throttle',
+    topLevelGroup: 'CPU',
+    group: 'CPU Frequency Throttling',
+  },
+  {
+    type: 'cpu_idle_state',
+    topLevelGroup: 'CPU',
+    group: 'CPU Idle State',
+  },
+  {
+    type: 'cpu_max_frequency_limit',
+    topLevelGroup: 'CPU',
+    group: 'CPU Max Frequency',
+  },
+  {
+    type: 'cpu_min_frequency_limit',
+    topLevelGroup: 'CPU',
+    group: 'CPU Min Frequency',
+  },
+  {
+    type: 'cpu_nr_running',
+    topLevelGroup: 'CPU',
+    group: 'CPU Number Running',
+  },
+  {
+    type: 'cpu_utilization',
+    topLevelGroup: 'CPU',
+    group: 'CPU Utilization',
+  },
+  {
+    type: 'cpustat',
+    topLevelGroup: 'CPU',
+    group: 'CPU Stat',
+  },
+  {
+    type: 'cros_ec_sensorhub_data',
+    topLevelGroup: 'HARDWARE',
+    group: 'ChromeOS EC Sensorhub',
+  },
+  {
+    type: 'diskstat',
+    topLevelGroup: 'IO',
+    group: 'Diskstat',
+  },
+  {
+    type: 'entity_state',
+    topLevelGroup: 'POWER',
+    group: 'Entity Residency',
+    shareYAxis: true,
+    mode: 'rate',
+  },
+  {
+    type: 'f2fs_iostat_latency',
+    topLevelGroup: 'IO',
+    group: 'F2FS IOStat Latency',
+  },
+  {
+    type: 'f2fs_iostat',
+    topLevelGroup: 'IO',
+    group: 'F2FS IOStat',
+  },
+  {
+    type: 'fastrpc_change',
+    topLevelGroup: 'PROCESS',
+    group: 'Fastrpc',
+  },
+  {
+    type: 'fastrpc',
+    topLevelGroup: 'HARDWARE',
+    group: 'Fastrpc',
+  },
+  {
+    type: 'fuchsia_counter',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'gpu_counter',
+    topLevelGroup: 'GPU',
+    group: 'GPU Counters',
+  },
+  {
+    type: 'gpu_memory',
+    topLevelGroup: 'GPU',
+    group: undefined,
+  },
+  {
+    type: 'ion_change',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+  {
+    type: 'ion',
+    topLevelGroup: 'MEMORY',
+    group: undefined,
+  },
+  {
+    type: 'json_counter_thread_fallback',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+  {
+    type: 'json_counter',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'linux_device_frequency',
+    topLevelGroup: 'HARDWARE',
+    group: 'Linux Device Frequency',
+  },
+  {
+    type: 'linux_rpm',
+    topLevelGroup: 'HARDWARE',
+    group: 'Linux RPM',
+  },
+  {
+    type: 'meminfo',
+    topLevelGroup: 'MEMORY',
+    group: 'Meminfo',
+  },
+  {
+    type: 'metatrace_counter',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+  {
+    type: 'mm_event_thread_fallback',
+    topLevelGroup: 'THREAD',
+    group: 'MM Event',
+  },
+  {
+    type: 'mm_event',
+    topLevelGroup: 'PROCESS',
+    group: 'MM Event',
+  },
+  {
+    type: 'net_kfree_skb',
+    topLevelGroup: 'NETWORK',
+    group: 'Network Packet Frees',
+  },
+  {
+    type: 'net_receive',
+    topLevelGroup: 'NETWORK',
+    group: 'Network Receive',
+    mode: 'rate',
+  },
+  {
+    type: 'net_transmit',
+    topLevelGroup: 'NETWORK',
+    group: 'Network Send',
+    mode: 'rate',
+  },
+  {
+    type: 'num_forks',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'num_irq_total',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'num_irq',
+    topLevelGroup: 'SYSTEM',
+    group: 'IRQ Count',
+  },
+  {
+    type: 'num_softirq_total',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'num_softirq',
+    topLevelGroup: 'SYSTEM',
+    group: 'Softirq Count',
+  },
+  {
+    type: 'oom_score_adj_thread_fallback',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+  {
+    type: 'oom_score_adj',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'perf_counter',
+    topLevelGroup: 'HARDWARE',
+    group: 'perf counters',
+  },
+  {
+    type: 'pixel_cpm_counters',
+    topLevelGroup: 'THERMALS',
+    group: 'CPM Counters',
+  },
+  {
+    type: 'power_rails',
+    group: 'Power Rails',
+    topLevelGroup: 'POWER',
+    shareYAxis: true,
+    mode: 'rate',
+  },
+  {
+    type: 'proc_stat_runtime',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'process_gpu_memory',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'process_memory_thread_fallback',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+  {
+    type: 'process_memory',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'psi',
+    group: 'PSI',
+    topLevelGroup: 'SYSTEM',
+    mode: 'rate',
+  },
+  {
+    type: 'screen_state',
+    topLevelGroup: 'SYSTEM',
+    group: 'Screen State',
+  },
+  {
+    type: 'smaps',
+    topLevelGroup: 'MEMORY',
+    group: 'smaps',
+  },
+  {
+    type: 'sysprop_counter',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'thermal_temperature_sys',
+    topLevelGroup: 'THERMALS',
+    group: 'Temperature (/sys)',
+  },
+  {
+    type: 'thermal_temperature',
+    topLevelGroup: 'THERMALS',
+    group: 'Temperature',
+  },
+  {
+    type: 'ufs_clkgating',
+    topLevelGroup: 'IO',
+    group: undefined,
+  },
+  {
+    type: 'ufs_command_count',
+    topLevelGroup: 'IO',
+    group: undefined,
+  },
+  {
+    type: 'virtgpu_latency',
+    topLevelGroup: 'GPU',
+    group: 'Virtgpu Latency',
+  },
+  {
+    type: 'virtgpu_num_free',
+    topLevelGroup: 'GPU',
+    group: 'Virtgpu num_free',
+  },
+  {
+    type: 'vmstat',
+    topLevelGroup: 'MEMORY',
+    group: 'vmstat',
+  },
+  {
+    type: 'vulkan_device_mem_allocation',
+    topLevelGroup: 'GPU',
+    group: 'Vulkan Allocations',
+  },
+  {
+    type: 'vulkan_device_mem_bind',
+    topLevelGroup: 'GPU',
+    group: 'Vulkan Binds',
+  },
+  {
+    type: 'vulkan_driver_mem',
+    topLevelGroup: 'GPU',
+    group: 'Vulkan Driver Memory',
+  },
+  {
+    type: 'battery_status',
+    topLevelGroup: 'POWER',
+    group: undefined,
+  },
+  {
+    type: 'battery_plugged_status',
+    topLevelGroup: 'POWER',
+    group: undefined,
+  },
+  {
+    type: 'ion',
+    topLevelGroup: 'MEMORY',
+    group: undefined,
+  },
+  {
+    type: 'ion_change',
+    topLevelGroup: 'MEMORY',
+    group: undefined,
+  },
+  {
+    type: 'android_dma_heap_change',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+];
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
new file mode 100644
index 0000000..afb7e9f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
@@ -0,0 +1,382 @@
+// Copyright (C) 2021 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 {assertExists} from '../../base/logging';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {getTrackName} from '../../public/utils';
+import {TrackNode} from '../../public/workspace';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
+import {CounterSelectionAggregator} from './counter_selection_aggregator';
+import {SLICE_TRACK_SCHEMAS} from './slice_tracks';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
+import {COUNTER_TRACK_SCHEMAS} from './counter_tracks';
+import {SliceSelectionAggregator} from './slice_selection_aggregator';
+import {TraceProcessorSliceTrack} from './trace_processor_slice_track';
+import {TopLevelTrackGroup, TrackGroupSchema} from './types';
+import {removeFalsyValues} from '../../base/array_utils';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.TraceProcessorTrack';
+  static readonly dependencies = [
+    ProcessThreadGroupsPlugin,
+    StandardGroupsPlugin,
+  ];
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    await this.addCounters(ctx);
+    await this.addSlices(ctx);
+
+    ctx.selection.registerAreaSelectionAggregator(
+      new CounterSelectionAggregator(),
+    );
+
+    ctx.selection.registerSqlSelectionResolver({
+      sqlTableName: 'slice',
+      callback: async (id: number) => {
+        const compatibleTypes = SLICE_TRACK_SCHEMAS.map(
+          (schema) => `'${schema.type}'`,
+        ).join(',');
+
+        // Locate the track for a given id in the slice table
+        const result = await ctx.engine.query(`
+          select
+            slice.track_id as trackId
+          from slice
+          join track on slice.track_id = track.id
+          where slice.id = ${id} and track.type in (${compatibleTypes})
+        `);
+
+        if (result.numRows() === 0) {
+          return undefined;
+        }
+        const {trackId} = result.firstRow({
+          trackId: NUM,
+        });
+        return {
+          trackUri: `/slice_${trackId}`,
+          eventId: id,
+        };
+      },
+    });
+
+    ctx.selection.registerAreaSelectionAggregator(
+      new SliceSelectionAggregator(),
+    );
+  }
+
+  private async addCounters(ctx: Trace) {
+    const result = await ctx.engine.query(`
+      include perfetto module viz.threads;
+
+      with tracks_summary as (
+        select
+          ct.type,
+          ct.name,
+          ct.id,
+          ct.unit,
+          extract_arg(ct.dimension_arg_set_id, 'utid') as utid,
+          extract_arg(ct.dimension_arg_set_id, 'upid') as upid
+        from counter_track ct
+        join _counter_track_summary using (id)
+        order by ct.name
+      )
+      select
+        s.*,
+        thread.tid,
+        thread.name as threadName,
+        ifnull(p.pid, tp.pid) as pid,
+        ifnull(p.name, tp.name) as processName,
+        ifnull(thread.is_main_thread, 0) as isMainThread,
+        ifnull(k.is_kernel_thread, 0) AS isKernelThread
+      from tracks_summary s
+      left join process p on s.upid = p.upid
+      left join thread using (utid)
+      left join _threads_with_kernel_flag k using (utid)
+      left join process tp on thread.upid = tp.upid
+      order by lower(s.name)
+    `);
+
+    const schemas = new Map(COUNTER_TRACK_SCHEMAS.map((x) => [x.type, x]));
+    const it = result.iter({
+      id: NUM,
+      type: STR,
+      name: STR_NULL,
+      unit: STR_NULL,
+      utid: NUM_NULL,
+      upid: NUM_NULL,
+      threadName: STR_NULL,
+      processName: STR_NULL,
+      tid: NUM_NULL,
+      pid: NUM_NULL,
+      isMainThread: NUM,
+      isKernelThread: NUM,
+    });
+    for (; it.valid(); it.next()) {
+      const {
+        type,
+        id: trackId,
+        name,
+        unit,
+        utid,
+        upid,
+        threadName,
+        processName,
+        tid,
+        pid,
+        isMainThread,
+        isKernelThread,
+      } = it;
+      const schema = schemas.get(type);
+      if (schema === undefined) {
+        continue;
+      }
+      const {group, topLevelGroup} = schema;
+      const title = getTrackName({
+        name,
+        tid,
+        threadName,
+        pid,
+        processName,
+        upid,
+        utid,
+        kind: COUNTER_TRACK_KIND,
+        threadTrack: utid !== undefined,
+      });
+      const uri = `/counter_${trackId}`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [trackId],
+          upid: upid ?? undefined,
+          utid: utid ?? undefined,
+          ...(isKernelThread === 1 && {kernelThread: true}),
+        },
+        chips: removeFalsyValues([
+          isKernelThread === 0 && isMainThread === 1 && 'main thread',
+        ]),
+        track: new TraceProcessorCounterTrack(
+          ctx,
+          uri,
+          {
+            yMode: schema.mode,
+            yRangeSharingKey: schema.shareYAxis ? it.type : undefined,
+            unit: unit ?? undefined,
+          },
+          trackId,
+          title,
+        ),
+      });
+      addTrack(
+        ctx,
+        topLevelGroup,
+        group,
+        upid,
+        utid,
+        new TrackNode({
+          uri,
+          title,
+          sortOrder: utid !== undefined || upid !== undefined ? 30 : 0,
+        }),
+      );
+    }
+  }
+
+  private async addSlices(ctx: Trace) {
+    const result = await ctx.engine.query(`
+      include perfetto module viz.threads;
+
+      with grouped as materialized (
+        select
+          t.type,
+          t.name,
+          extract_arg(t.dimension_arg_set_id, 'utid') as utid,
+          extract_arg(t.dimension_arg_set_id, 'upid') as upid,
+          group_concat(t.id) as trackIds,
+          count() as trackCount
+        from _slice_track_summary s
+        join track t using (id)
+        group by type, upid, utid, name
+      )
+      select
+        s.type,
+        s.name,
+        s.utid,
+        ifnull(s.upid, tp.upid) as upid,
+        s.trackIds as trackIds,
+        __max_layout_depth(s.trackCount, s.trackIds) as maxDepth,
+        thread.tid,
+        thread.name as threadName,
+        ifnull(p.pid, tp.pid) as pid,
+        ifnull(p.name, tp.name) as processName,
+        ifnull(thread.is_main_thread, 0) as isMainThread,
+        ifnull(k.is_kernel_thread, 0) AS isKernelThread
+      from grouped s
+      left join process p on s.upid = p.upid
+      left join thread using (utid)
+      left join _threads_with_kernel_flag k using (utid)
+      left join process tp on thread.upid = tp.upid
+      order by lower(s.name)
+    `);
+
+    const schemas = new Map(SLICE_TRACK_SCHEMAS.map((x) => [x.type, x]));
+    const it = result.iter({
+      type: STR,
+      name: STR_NULL,
+      utid: NUM_NULL,
+      upid: NUM_NULL,
+      trackIds: STR,
+      maxDepth: NUM,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+      pid: NUM_NULL,
+      processName: STR_NULL,
+      isMainThread: NUM,
+      isKernelThread: NUM,
+    });
+    for (; it.valid(); it.next()) {
+      const {
+        trackIds: rawTrackIds,
+        type,
+        name,
+        maxDepth,
+        utid,
+        upid,
+        threadName,
+        processName,
+        tid,
+        pid,
+        isMainThread,
+        isKernelThread,
+      } = it;
+      const schema = schemas.get(type);
+      if (schema === undefined) {
+        continue;
+      }
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const {group, topLevelGroup} = schema;
+      const title = getTrackName({
+        name,
+        tid,
+        threadName,
+        pid,
+        processName,
+        upid,
+        utid,
+        kind: SLICE_TRACK_KIND,
+        threadTrack: utid !== undefined,
+      });
+      const uri = `/slice_${trackIds[0]}`;
+      ctx.tracks.registerTrack({
+        uri,
+        title,
+        tags: {
+          kind: SLICE_TRACK_KIND,
+          trackIds: trackIds,
+          upid: upid ?? undefined,
+          utid: utid ?? undefined,
+          ...(isKernelThread === 1 && {kernelThread: true}),
+        },
+        chips: removeFalsyValues([
+          isKernelThread === 0 && isMainThread === 1 && 'main thread',
+        ]),
+        track: new TraceProcessorSliceTrack(ctx, uri, maxDepth, trackIds),
+      });
+      addTrack(
+        ctx,
+        topLevelGroup,
+        group,
+        upid,
+        utid,
+        new TrackNode({
+          uri,
+          title,
+          sortOrder: utid !== undefined || upid !== undefined ? 20 : 0,
+        }),
+      );
+    }
+  }
+}
+
+function addTrack(
+  ctx: Trace,
+  topLevelGroup: TopLevelTrackGroup,
+  group: string | TrackGroupSchema | undefined,
+  upid: number | null,
+  utid: number | null,
+  track: TrackNode,
+) {
+  switch (topLevelGroup) {
+    case 'PROCESS': {
+      const process = assertExists(
+        ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForProcess(assertExists(upid)),
+      );
+      getGroupByName(process, group, upid).addChildInOrder(track);
+      break;
+    }
+    case 'THREAD': {
+      const thread = assertExists(
+        ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForThread(assertExists(utid)),
+      );
+      getGroupByName(thread, group, utid).addChildInOrder(track);
+      break;
+    }
+    case undefined: {
+      getGroupByName(ctx.workspace.tracks, group, upid).addChildInOrder(track);
+      break;
+    }
+    default: {
+      const standardGroup = ctx.plugins
+        .getPlugin(StandardGroupsPlugin)
+        .getOrCreateStandardGroup(ctx.workspace, topLevelGroup);
+      getGroupByName(standardGroup, group, null).addChildInOrder(track);
+      break;
+    }
+  }
+}
+
+function getGroupByName(
+  node: TrackNode,
+  group: string | TrackGroupSchema | undefined,
+  scopeId: number | null,
+) {
+  if (group === undefined) {
+    return node;
+  }
+  const name = typeof group === 'string' ? group : group.name;
+  const expanded = typeof group === 'string' ? false : group.expanded ?? false;
+  const groupId = `tp_group_${scopeId}_${name.toLowerCase().replace(' ', '_')}`;
+  const groupNode = node.getTrackById(groupId);
+  if (groupNode) {
+    return groupNode;
+  }
+  const newGroup = new TrackNode({
+    uri: `/${group}`,
+    id: groupId,
+    isSummary: true,
+    title: name,
+    collapsed: !expanded,
+  });
+  node.addChildInOrder(newGroup);
+  return newGroup;
+}
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
similarity index 74%
rename from ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
index 23226bc..5561533 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_selection_aggregator.ts
@@ -13,30 +13,28 @@
 // limitations under the License.
 
 import {ColumnDef, Sorting} from '../../public/aggregation';
-import {AreaSelection} from '../../public/selection';
+import {AreaSelection, AreaSelectionAggregator} from '../../public/selection';
+import {Dataset} from '../../trace_processor/dataset';
 import {Engine} from '../../trace_processor/engine';
-import {AreaSelectionAggregator} from '../../public/selection';
-import {UnionDataset} from '../../trace_processor/dataset';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 
 export class SliceSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'slice_aggregation';
 
-  async createAggregateView(engine: Engine, area: AreaSelection) {
-    const desiredSchema = {
-      id: NUM,
-      name: STR,
-      ts: LONG,
-      dur: LONG,
-    };
-    const validDatasets = area.tracks
-      .map((track) => track.track.getDataset?.())
-      .filter((ds) => ds !== undefined)
-      .filter((ds) => ds.implements(desiredSchema));
-    if (validDatasets.length === 0) {
-      return false;
-    }
-    const unionDataset = new UnionDataset(validDatasets);
+  readonly schema = {
+    id: NUM,
+    name: STR,
+    ts: LONG,
+    dur: LONG,
+  } as const;
+
+  async createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ) {
+    if (!dataset) return false;
+
     await engine.query(`
       create or replace perfetto table ${this.id} as
       select
@@ -44,7 +42,7 @@
         sum(dur) AS total_dur,
         sum(dur)/count() as avg_dur,
         count() as occurrences
-        from (${unionDataset.optimize().query()})
+        from (${dataset.query()})
       where
         ts + dur > ${area.start}
         and ts < ${area.end}
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts
new file mode 100644
index 0000000..bbc9aa9
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/slice_tracks.ts
@@ -0,0 +1,229 @@
+// Copyright (C) 2025 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 {StandardGroup} from '../dev.perfetto.StandardGroups';
+
+export interface SliceTrackGroupSchema {
+  name: string;
+  expanded?: true;
+}
+
+interface SliceTrackTypeSchema {
+  type: string;
+  group: string | SliceTrackGroupSchema | undefined;
+  topLevelGroup: 'PROCESS' | 'THREAD' | StandardGroup | undefined;
+}
+
+export const SLICE_TRACK_SCHEMAS: ReadonlyArray<SliceTrackTypeSchema> = [
+  {
+    type: 'battery_stats',
+    topLevelGroup: 'POWER',
+    group: 'Battery Stats',
+  },
+  {
+    type: 'legacy_async_process_slice',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'legacy_async_global_slice',
+    topLevelGroup: undefined,
+    group: 'Global Legacy Events',
+  },
+  {
+    type: 'legacy_chrome_global_instants',
+    group: undefined,
+    topLevelGroup: undefined,
+  },
+  {
+    type: 'android_device_state',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'android_lmk',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'chrome_process_instant',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'drm_vblank',
+    topLevelGroup: 'HARDWARE',
+    group: 'DRM VBlank',
+  },
+  {
+    type: 'drm_sched_ring',
+    topLevelGroup: 'HARDWARE',
+    group: 'DRM Sched Ring',
+  },
+  {
+    type: 'drm_fence',
+    topLevelGroup: 'HARDWARE',
+    group: 'DRM Fence',
+  },
+  {
+    type: 'interconnect_events',
+    topLevelGroup: 'HARDWARE',
+    group: undefined,
+  },
+  {
+    type: 'cpu_irq',
+    topLevelGroup: 'CPU',
+    group: 'IRQs',
+  },
+  {
+    type: 'cpu_softirq',
+    topLevelGroup: 'CPU',
+    group: 'Softirqs',
+  },
+  {
+    type: 'net_socket_set_state',
+    topLevelGroup: 'NETWORK',
+    group: 'Socket Set State',
+  },
+  {
+    type: 'net_tcp_retransmit_skb',
+    topLevelGroup: 'NETWORK',
+    group: 'TCP Retransmit SKB',
+  },
+  {
+    type: 'cpu_napi_gro',
+    topLevelGroup: 'CPU',
+    group: 'NAPI GRO',
+  },
+  {
+    type: 'ufs_command_tag',
+    topLevelGroup: 'IO',
+    group: 'UFS Command Tag',
+  },
+  {
+    type: 'wakesource_wakelock',
+    topLevelGroup: 'POWER',
+    group: 'Kernel Wakelocks',
+  },
+  {
+    type: 'dumpstate_wakelocks',
+    topLevelGroup: 'POWER',
+    group: 'Kernel Wakelocks',
+  },
+  {
+    type: 'cpu_funcgraph',
+    topLevelGroup: 'CPU',
+    group: 'Funcgraph',
+  },
+  {
+    type: 'android_ion_allocations',
+    topLevelGroup: 'MEMORY',
+    group: 'ION',
+  },
+  {
+    type: 'android_fs',
+    topLevelGroup: 'IO',
+    group: undefined,
+  },
+  {
+    type: 'cpu_mali_irq',
+    topLevelGroup: 'CPU',
+    group: undefined,
+  },
+  {
+    type: 'mali_mcu_state',
+    topLevelGroup: 'GPU',
+    group: undefined,
+  },
+  {
+    type: 'pkvm_hypervisor',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'virtgpu_queue_event',
+    topLevelGroup: 'GPU',
+    group: 'Virtio GPU Events',
+  },
+  {
+    type: 'virtio_video_queue_event',
+    topLevelGroup: 'SYSTEM',
+    group: 'Virtio Video Queue Events',
+  },
+  {
+    type: 'virtio_video_command',
+    topLevelGroup: 'SYSTEM',
+    group: 'Virtio Video Command Events',
+  },
+  {
+    type: 'android_camera_event',
+    topLevelGroup: 'HARDWARE',
+    group: undefined,
+  },
+  {
+    type: 'gpu_render_stage',
+    topLevelGroup: 'GPU',
+    group: 'Render Stage',
+  },
+  {
+    type: 'vulkan_events',
+    topLevelGroup: 'GPU',
+    group: undefined,
+  },
+  {
+    type: 'gpu_log',
+    topLevelGroup: 'GPU',
+    group: undefined,
+  },
+  {
+    type: 'graphics_frame_event',
+    topLevelGroup: 'GPU',
+    group: undefined,
+  },
+  {
+    type: 'triggers',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'network_packets',
+    topLevelGroup: 'NETWORK',
+    group: undefined,
+  },
+  {
+    type: 'pixel_modem_event',
+    topLevelGroup: 'HARDWARE',
+    group: undefined,
+  },
+  {
+    type: 'statsd_atoms',
+    topLevelGroup: 'SYSTEM',
+    group: undefined,
+  },
+  {
+    type: 'atrace_async_slice',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'atrace_async_slice_for_track',
+    topLevelGroup: 'PROCESS',
+    group: undefined,
+  },
+  {
+    type: 'thread_execution',
+    topLevelGroup: 'THREAD',
+    group: undefined,
+  },
+];
diff --git a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
similarity index 89%
rename from ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
index 61f257e..e0be4ac 100644
--- a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_counter_track.ts
@@ -81,16 +81,24 @@
   // SHOULD assume `rootTable` has an id column is another matter...
   async getSelectionDetails(id: number): Promise<TrackEventDetails> {
     const query = `
-      WITH 
-        CTE AS (
+      WITH CTE AS (
+        SELECT
+          id,
+          ts as leftTs
+        FROM ${this.rootTable}
+        WHERE track_id = ${this.trackId} AND id = ${id}
+      )
+      SELECT
+        *,
+        (
           SELECT
-            id,
-            ts as leftTs,
-            LEAD(ts) OVER (ORDER BY ts) AS rightTs
+            ts
           FROM ${this.rootTable}
-          WHERE track_id = ${this.trackId}
-        )
-      SELECT * FROM CTE WHERE id = ${id}
+          WHERE track_id = ${this.trackId} AND ts > leftTs
+          ORDER BY ts ASC
+          LIMIT 1
+        ) as rightTs
+      FROM CTE
     `;
 
     const counter = await this.engine.query(query);
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
similarity index 95%
rename from ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
index 46db758..6794d93 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/trace_processor_slice_track.ts
@@ -40,11 +40,14 @@
 };
 export type ThreadSliceRow = typeof THREAD_SLICE_ROW;
 
-export class AsyncSliceTrack extends NamedSliceTrack<Slice, ThreadSliceRow> {
+export class TraceProcessorSliceTrack extends NamedSliceTrack<
+  Slice,
+  ThreadSliceRow
+> {
   constructor(
     trace: Trace,
     uri: string,
-    maxDepth: number,
+    maxDepth: number | undefined,
     private readonly trackIds: number[],
   ) {
     super(trace, uri);
diff --git a/ui/src/widgets/raf.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
similarity index 66%
rename from ui/src/widgets/raf.ts
rename to ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
index dc0d3ab..b2956b3 100644
--- a/ui/src/widgets/raf.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/types.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2025 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.
@@ -12,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-let FULL_REDRAW_FUNCTION = (_force?: 'force') => {};
+import {StandardGroup} from '../dev.perfetto.StandardGroups';
 
-export function setScheduleFullRedraw(func: () => void) {
-  FULL_REDRAW_FUNCTION = func;
-}
+export type TopLevelTrackGroup =
+  | 'PROCESS'
+  | 'THREAD'
+  | StandardGroup
+  | undefined;
 
-export function scheduleFullRedraw(force?: 'force') {
-  FULL_REDRAW_FUNCTION(force);
+export interface TrackGroupSchema {
+  name: string;
+  expanded?: true;
 }
diff --git a/ui/src/plugins/dev.perfetto.TrackEvent/index.ts b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts
new file mode 100644
index 0000000..a72f765
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.TrackEvent/index.ts
@@ -0,0 +1,236 @@
+// Copyright (C) 2025 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 {Trace} from '../../public/trace';
+import {PerfettoPlugin} from '../../public/plugin';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
+import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
+import {TrackNode} from '../../public/workspace';
+import {assertExists, assertTrue} from '../../base/logging';
+import {COUNTER_TRACK_KIND, SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
+import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
+import {getTrackName} from '../../public/utils';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.TrackEvent';
+  static readonly dependencies = [
+    ProcessThreadGroupsPlugin,
+    TraceProcessorTrackPlugin,
+  ];
+
+  private readonly trackIdToUri = new Map<number, string>();
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const res = await ctx.engine.query(`
+      include perfetto module viz.summary.track_event;
+      select
+        ifnull(g.upid, t.upid) as upid,
+        g.utid,
+        g.parent_id as parentId,
+        g.is_counter AS isCounter,
+        g.name,
+        g.unit,
+        g.builtin_counter_type as builtinCounterType,
+        g.has_data AS hasData,
+        g.has_children AS hasChildren,
+        g.track_ids as trackIds,
+        g.order_id as orderId,
+        t.name as threadName,
+        t.tid as tid,
+        ifnull(p.pid, tp.pid) as pid,
+        ifnull(p.name, tp.name) as processName
+      from _track_event_tracks_ordered_groups g
+      left join process p using (upid)
+      left join thread t using (utid)
+      left join process tp on tp.upid = t.upid
+    `);
+    const it = res.iter({
+      upid: NUM_NULL,
+      utid: NUM_NULL,
+      parentId: NUM_NULL,
+      isCounter: NUM,
+      name: STR_NULL,
+      unit: STR_NULL,
+      builtinCounterType: STR_NULL,
+      hasData: NUM,
+      hasChildren: NUM,
+      trackIds: STR,
+      orderId: NUM,
+      threadName: STR_NULL,
+      tid: NUM_NULL,
+      pid: NUM_NULL,
+      processName: STR_NULL,
+    });
+    const processGroupsPlugin = ctx.plugins.getPlugin(
+      ProcessThreadGroupsPlugin,
+    );
+    const trackIdToTrackNode = new Map<number, TrackNode>();
+    for (; it.valid(); it.next()) {
+      const {
+        upid,
+        utid,
+        parentId,
+        isCounter,
+        name,
+        unit,
+        builtinCounterType,
+        hasData,
+        hasChildren,
+        trackIds: rawTrackIds,
+        orderId,
+        threadName,
+        tid,
+        pid,
+        processName,
+      } = it;
+
+      // Don't add track_event tracks which don't have any data and don't have
+      // any children.
+      if (!hasData && !hasChildren) {
+        continue;
+      }
+
+      const kind = isCounter ? COUNTER_TRACK_KIND : SLICE_TRACK_KIND;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const title = getTrackName({
+        name,
+        utid,
+        upid,
+        kind,
+        threadTrack: utid !== null,
+        threadName,
+        processName,
+        tid,
+        pid,
+      });
+      const uri = `/track_event_${trackIds[0]}`;
+      if (hasData && isCounter) {
+        // Don't show any builtin counter.
+        if (builtinCounterType !== null) {
+          continue;
+        }
+        assertTrue(trackIds.length === 1);
+        const trackId = trackIds[0];
+        this.trackIdToUri.set(trackId, uri);
+        ctx.tracks.registerTrack({
+          uri,
+          title,
+          tags: {
+            kind,
+            trackIds: [trackIds[0]],
+            upid: upid ?? undefined,
+            utid: utid ?? undefined,
+          },
+          track: new TraceProcessorCounterTrack(
+            ctx,
+            uri,
+            {
+              unit: unit ?? undefined,
+            },
+            trackId,
+            title,
+          ),
+        });
+      } else if (hasData) {
+        for (const trackId of trackIds) {
+          this.trackIdToUri.set(trackId, uri);
+        }
+        ctx.tracks.registerTrack({
+          uri,
+          title,
+          tags: {
+            kind,
+            trackIds: trackIds,
+            upid: upid ?? undefined,
+            utid: utid ?? undefined,
+          },
+          track: new TraceProcessorSliceTrack(ctx, uri, undefined, trackIds),
+        });
+      }
+      const parent = findParentTrackNode(
+        ctx,
+        processGroupsPlugin,
+        trackIdToTrackNode,
+        parentId ?? undefined,
+        upid ?? undefined,
+        utid ?? undefined,
+        hasChildren,
+      );
+      const node = new TrackNode({
+        title,
+        sortOrder: orderId,
+        isSummary: hasData === 0,
+        uri: uri,
+      });
+      parent.addChildInOrder(node);
+      trackIdToTrackNode.set(trackIds[0], node);
+    }
+
+    ctx.selection.registerSqlSelectionResolver({
+      sqlTableName: 'slice',
+      callback: async (eventId: number) => {
+        const res = await ctx.engine.query(`
+          select
+            track_id as trackId
+          from slice
+          where slice.id = ${eventId}
+        `);
+        const firstRow = res.maybeFirstRow({
+          trackId: NUM,
+        });
+        if (!firstRow) return undefined;
+        const trackId = firstRow.trackId;
+        const trackUri = this.trackIdToUri.get(trackId);
+        if (!trackUri) return undefined;
+        return {trackUri, eventId};
+      },
+    });
+  }
+}
+
+function findParentTrackNode(
+  ctx: Trace,
+  processGroupsPlugin: ProcessThreadGroupsPlugin,
+  trackIdToTrackNode: Map<number, TrackNode>,
+  parentId: number | undefined,
+  upid: number | undefined,
+  utid: number | undefined,
+  hasChildren: number,
+): TrackNode {
+  if (parentId !== undefined) {
+    return assertExists(trackIdToTrackNode.get(parentId));
+  }
+  if (utid !== undefined) {
+    return assertExists(processGroupsPlugin.getGroupForThread(utid));
+  }
+  if (upid !== undefined) {
+    return assertExists(processGroupsPlugin.getGroupForProcess(upid));
+  }
+  if (hasChildren) {
+    return ctx.workspace.tracks;
+  }
+  const id = `/track_event_root`;
+  let node = ctx.workspace.getTrackById(id);
+  if (node === undefined) {
+    node = new TrackNode({
+      id,
+      title: 'Global Track Events',
+      isSummary: true,
+    });
+    ctx.workspace.addChildInOrder(node);
+  }
+  return node;
+}
diff --git a/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
index 7a76d76..97b6ada 100644
--- a/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
+++ b/ui/src/plugins/dev.perfetto.VizPage/viz_page.ts
@@ -41,7 +41,6 @@
         initialText: attrs.spec,
         onUpdate: (text: string) => {
           attrs.setSpec(text);
-          attrs.trace.scheduleFullRedraw();
         },
       }),
     );
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index c1306d6..57b1018 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -55,10 +55,9 @@
 import {SegmentedButtons} from '../../widgets/segmented_buttons';
 import {MiddleEllipsis} from '../../widgets/middle_ellipsis';
 import {Chip, ChipBar} from '../../widgets/chip';
-import {TrackWidget} from '../../widgets/track_widget';
-import {scheduleFullRedraw} from '../../widgets/raf';
+import {TrackShell} from '../../widgets/track_shell';
 import {CopyableLink} from '../../widgets/copyable_link';
-import {VirtualOverlayCanvas} from '../../widgets/virtual_overlay_canvas';
+import {VirtualOverlayCanvas} from '../../components/widgets/virtual_overlay_canvas';
 import {SplitPanel} from '../../widgets/split_panel';
 import {TabbedSplitPanel} from '../../widgets/tabbed_split_panel';
 
@@ -313,7 +312,6 @@
           intent: Intent.Primary,
           onclick: () => {
             portalOpen = !portalOpen;
-            scheduleFullRedraw();
           },
         }),
         portalOpen &&
@@ -365,7 +363,6 @@
           label: 'Close Popup',
           onclick: () => {
             popupOpen = !popupOpen;
-            scheduleFullRedraw();
           },
         }),
       );
@@ -501,7 +498,6 @@
       label: key,
       onchange: () => {
         this.optValues[key] = !Boolean(this.optValues[key]);
-        scheduleFullRedraw();
       },
     });
   }
@@ -515,7 +511,6 @@
         value: this.optValues[key],
         oninput: (e: Event) => {
           this.optValues[key] = (e.target as HTMLInputElement).value;
-          scheduleFullRedraw();
         },
       }),
     );
@@ -533,7 +528,6 @@
           this.optValues[key] = Number.parseInt(
             (e.target as HTMLInputElement).value,
           );
-          scheduleFullRedraw();
         },
       }),
     );
@@ -553,7 +547,6 @@
           onchange: (e: Event) => {
             const el = e.target as HTMLSelectElement;
             this.optValues[key] = el.value;
-            scheduleFullRedraw();
           },
         },
         optionElements,
@@ -645,14 +638,12 @@
         onTagAdd: (tag) => {
           tags.push(tag);
           tagInputValue = '';
-          scheduleFullRedraw();
         },
         onChange: (value) => {
           tagInputValue = value;
         },
         onTagRemove: (index) => {
           tags.splice(index, 1);
-          scheduleFullRedraw();
         },
       });
     },
@@ -669,7 +660,6 @@
         selectedOption: selectedIdx,
         onOptionSelected: (num) => {
           selectedIdx = num;
-          scheduleFullRedraw();
         },
       });
     },
@@ -869,7 +859,6 @@
               diffs.forEach(({id, checked}) => {
                 options[id] = checked;
               });
-              scheduleFullRedraw();
             },
             ...rest,
           }),
@@ -896,7 +885,6 @@
               diffs.forEach(({id, checked}) => {
                 options[id] = checked;
               });
-              scheduleFullRedraw();
             },
             ...rest,
           }),
@@ -1028,7 +1016,7 @@
                     {
                       icon: Icons.ContextMenu,
                     },
-                    'SELECT * FROM raw WHERE id = 123',
+                    'SELECT * FROM ftrace_event WHERE id = 123',
                   ),
                 },
                 m(MenuItem, {
@@ -1276,7 +1264,6 @@
                 offset: rowOffset,
                 rows,
               };
-              scheduleFullRedraw();
             },
           };
           return m(VirtualTable, attrs);
@@ -1333,22 +1320,29 @@
         },
       }),
       m(WidgetShowcase, {
-        label: 'Track',
-        description: `The shell and content DOM elements of a track.`,
+        label: 'TrackShell',
+        description: `The Mithril parts of a track (the shell, mainly).`,
         renderWidget: (opts) => {
-          const {buttons, chips, multipleTracks, ...rest} = opts;
+          const {buttons, chips, multipleTracks, error, ...rest} = opts;
           const dummyButtons = () => [
             m(Button, {icon: 'info', compact: true}),
             m(Button, {icon: 'settings', compact: true}),
           ];
           const dummyChips = () => ['foo', 'bar'];
 
-          const renderTrack = () =>
-            m(TrackWidget, {
-              buttons: Boolean(buttons) ? dummyButtons() : undefined,
-              chips: Boolean(chips) ? dummyChips() : undefined,
-              ...rest,
-            });
+          const renderTrack = (children?: m.Children) =>
+            m(
+              TrackShell,
+              {
+                buttons: Boolean(buttons) ? dummyButtons() : undefined,
+                chips: Boolean(chips) ? dummyChips() : undefined,
+                error: Boolean(error)
+                  ? new Error('An error has occurred')
+                  : undefined,
+                ...rest,
+              },
+              children,
+            );
 
           return m(
             '',
@@ -1366,14 +1360,15 @@
           buttons: true,
           chips: true,
           heightPx: 32,
-          indentationLevel: 3,
           collapsible: true,
           collapsed: true,
-          isSummary: false,
+          summary: false,
           highlight: false,
           error: false,
           multipleTracks: false,
           reorderable: false,
+          depth: 0,
+          lite: false,
         },
       }),
       m(WidgetShowcase, {
@@ -1510,7 +1505,6 @@
         },
         view: function (vnode: m.Vnode<{}, {progress: number}>) {
           vnode.state.progress = (vnode.state.progress + 1) % 100;
-          scheduleFullRedraw();
           return m(
             'div',
             m('div', 'You should see an animating progress bar'),
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
index 5f026db..5eeacc8 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
@@ -45,7 +45,7 @@
 } from './scroll_jank_cause_link_utils';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
 import {sliceRef} from '../../components/widgets/slice';
-import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {JANKS_TRACK_URI, renderSliceRef} from './utils';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {Trace} from '../../public/trace';
 
@@ -161,7 +161,6 @@
       this.trace.engine,
       asSliceSqlId(this.id),
     );
-    this.trace.scheduleFullRedraw();
   }
 
   async loadJankSlice() {
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
index cc5affc..fd1edf5 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
@@ -35,7 +35,7 @@
     await this.addEventLatencyTrack(ctx, group);
     await this.addScrollJankV3ScrollTrack(ctx, group);
     await ScrollJankCauseMap.initialize(ctx.engine);
-    this.addScrollTimelineTrack(ctx, group);
+    await this.addScrollTimelineTrack(ctx, group);
     ctx.workspace.addChildInOrder(group);
     group.expand();
   }
@@ -193,14 +193,21 @@
     group.addChildInOrder(track);
   }
 
-  private addScrollTimelineTrack(ctx: Trace, group: TrackNode) {
+  private async addScrollTimelineTrack(
+    ctx: Trace,
+    group: TrackNode,
+  ): Promise<void> {
     const uri = 'org.chromium.ChromeScrollJank#scrollTimeline';
     const title = 'Chrome Scroll Timeline';
 
+    const tableName =
+      'scrolltimelinetrack_org_chromium_ChromeScrollJank_scrollTimeline';
+    await ScrollTimelineTrack.createTableForTrack(ctx, tableName);
+
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new ScrollTimelineTrack(ctx, uri),
+      track: new ScrollTimelineTrack(ctx, uri, tableName),
     });
 
     const track = new TrackNode({uri, title});
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
index 5f572f5..fad569d 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor} from '../../public/color';
+import {HSLColor} from '../../base/color';
 import {makeColorScheme} from '../../components/colorizer';
 
 export const JANK_COLOR = makeColorScheme(new HSLColor([343, 100, 43]));
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
index e68e58b..4ef92f0 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
@@ -43,7 +43,7 @@
   getPredictorJankDeltas,
   getPresentedScrollDeltas,
 } from './scroll_delta_graph';
-import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {JANKS_TRACK_URI, renderSliceRef} from './utils';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {Trace} from '../../public/trace';
 
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
index 410e635..2e007ef 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
@@ -27,7 +27,7 @@
 import {SqlRef} from '../../widgets/sql_ref';
 import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
 import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils';
+import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './utils';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {Trace} from '../../public/trace';
 
@@ -137,9 +137,7 @@
     };
 
     await this.loadJankyFrames();
-
     await this.loadSlices();
-    this.trace.scheduleFullRedraw();
   }
 
   private hasCause(): boolean {
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts
new file mode 100644
index 0000000..6f9a75d
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_details_panel.ts
@@ -0,0 +1,219 @@
+// Copyright (C) 2025 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 {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
+import {LONG, NUM_NULL, STR} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Duration, duration, Time, time} from '../../base/time';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Section} from '../../widgets/section';
+import {Tree, TreeNode} from '../../widgets/tree';
+import {Timestamp} from '../../components/widgets/timestamp';
+import {DurationWidget} from '../../components/widgets/duration';
+import {SqlRef} from '../../widgets/sql_ref';
+import {asSliceSqlId} from '../../components/sql_utils/core_types';
+import {fromSqlBool} from './utils';
+
+export class ScrollTimelineDetailsPanel implements TrackEventDetailsPanel {
+  // Information about the scroll update *slice*, which was emitted by
+  // ScrollTimelineTrack.
+  // Source: this.tableName[id=this.id]
+  private sliceData?: {
+    name: string;
+    ts: time;
+    dur: duration;
+    // ID of the scroll update in chrome_scroll_update_info.
+    scrollUpdateId: bigint;
+  };
+
+  // Information about the scroll *update*, which comes from the Chrome tracing
+  // stdlib.
+  // Source: chrome_scroll_update_info[id=this.sliceData.scrollUpdateId]
+  private scrollData?: {
+    vsyncInterval: duration | undefined;
+    isPresented: boolean | undefined;
+    isJanky: boolean | undefined;
+    isInertial: boolean | undefined;
+    isFirstScrollUpdateInScroll: boolean | undefined;
+    isFirstScrollUpdateInFrame: boolean | undefined;
+  };
+
+  constructor(
+    private readonly trace: Trace,
+    private readonly tableName: string,
+    // ID of the slice in tableName.
+    private readonly id: number,
+  ) {}
+
+  async load(): Promise<void> {
+    await this.querySliceData();
+    await this.queryScrollData();
+  }
+
+  private async querySliceData(): Promise<void> {
+    assertTrue(this.sliceData === undefined);
+    const queryResult = await this.trace.engine.query(`
+      SELECT
+        name,
+        ts,
+        dur,
+        scroll_update_id
+      FROM ${this.tableName}
+      WHERE id = ${this.id}`);
+    const row = queryResult.firstRow({
+      name: STR,
+      ts: LONG,
+      dur: LONG,
+      scroll_update_id: LONG,
+    });
+    this.sliceData = {
+      name: row.name,
+      ts: Time.fromRaw(row.ts),
+      dur: Duration.fromRaw(row.dur),
+      scrollUpdateId: row.scroll_update_id,
+    };
+  }
+
+  private async queryScrollData(): Promise<void> {
+    assertExists(this.sliceData);
+    assertTrue(this.scrollData === undefined);
+    const queryResult = await this.trace.engine.query(`
+      INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+      SELECT
+        vsync_interval_ms,
+        is_presented,
+        is_janky,
+        is_inertial,
+        is_first_scroll_update_in_scroll,
+        is_first_scroll_update_in_frame
+      FROM chrome_scroll_update_info
+      WHERE id = ${this.sliceData!.scrollUpdateId}`);
+    const row = queryResult.firstRow({
+      vsync_interval_ms: NUM_NULL,
+      is_presented: NUM_NULL,
+      is_janky: NUM_NULL,
+      is_inertial: NUM_NULL,
+      is_first_scroll_update_in_scroll: NUM_NULL,
+      is_first_scroll_update_in_frame: NUM_NULL,
+    });
+    this.scrollData = {
+      vsyncInterval:
+        row.vsync_interval_ms === null
+          ? undefined
+          : Duration.fromMillis?.(row.vsync_interval_ms),
+      isPresented: fromSqlBool(row.is_presented),
+      isJanky: fromSqlBool(row.is_janky),
+      isInertial: fromSqlBool(row.is_inertial),
+      isFirstScrollUpdateInScroll: fromSqlBool(
+        row.is_first_scroll_update_in_scroll,
+      ),
+      isFirstScrollUpdateInFrame: fromSqlBool(
+        row.is_first_scroll_update_in_frame,
+      ),
+    };
+  }
+
+  render(): m.Children {
+    return m(
+      DetailsShell,
+      {
+        title: 'Slice',
+        description: this.sliceData?.name ?? 'Loading...',
+      },
+      m(
+        GridLayout,
+        m(GridLayoutColumn, this.renderSliceDetails()),
+        m(GridLayoutColumn, this.renderScrollDetails()),
+      ),
+    );
+  }
+
+  private renderSliceDetails(): m.Child {
+    let child;
+    if (this.sliceData === undefined) {
+      child = 'Loading...';
+    } else {
+      child = m(
+        Tree,
+        m(TreeNode, {
+          left: 'Name',
+          right: this.sliceData.name,
+        }),
+        m(TreeNode, {
+          left: 'Start time',
+          right: m(Timestamp, {ts: this.sliceData.ts}),
+        }),
+        m(TreeNode, {
+          left: 'Duration',
+          right: m(DurationWidget, {dur: this.sliceData.dur}),
+        }),
+        m(TreeNode, {
+          left: 'SQL ID',
+          right: m(SqlRef, {
+            table: this.tableName,
+            id: asSliceSqlId(this.id),
+          }),
+        }),
+      );
+    }
+    return m(Section, {title: 'Slice details'}, child);
+  }
+
+  private renderScrollDetails(): m.Child {
+    let child;
+    if (this.sliceData === undefined || this.scrollData === undefined) {
+      child = 'Loading...';
+    } else {
+      child = m(
+        Tree,
+        m(TreeNode, {
+          left: 'Vsync interval',
+          right:
+            this.scrollData.vsyncInterval === undefined
+              ? `${this.scrollData.vsyncInterval}`
+              : m(DurationWidget, {dur: this.scrollData.vsyncInterval}),
+        }),
+        m(TreeNode, {
+          left: 'Is presented',
+          right: `${this.scrollData.isPresented}`,
+        }),
+        m(TreeNode, {
+          left: 'Is janky',
+          right: `${this.scrollData.isJanky}`,
+        }),
+        m(TreeNode, {
+          left: 'Is inertial',
+          right: `${this.scrollData.isInertial}`,
+        }),
+        m(TreeNode, {
+          left: 'Is first scroll update in scroll',
+          right: `${this.scrollData.isFirstScrollUpdateInScroll}`,
+        }),
+        m(TreeNode, {
+          left: 'Is first scroll update in frame',
+          right: `${this.scrollData.isFirstScrollUpdateInFrame}`,
+        }),
+        m(TreeNode, {
+          left: 'SQL ID',
+          // TODO: b/383990024 - Make this a clickable reference.
+          right: `chrome_scroll_update_info[${this.sliceData.scrollUpdateId}]`,
+        }),
+      );
+    }
+    return m(Section, {title: 'Scroll details'}, child);
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
index a5fb4b6..081feda 100644
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_timeline_track.ts
@@ -12,164 +12,205 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createPerfettoTable} from '../../trace_processor/sql_utils';
 import {generateSqlWithInternalLayout} from '../../components/sql_utils/layout';
 import {
   NAMED_ROW,
-  NamedRow,
   NamedSliceTrack,
 } from '../../components/tracks/named_slice_track';
 import {Slice} from '../../public/track';
-import {sqlNameSafe} from '../../base/string_utils';
-import {SqlTableSliceTrackDetailsPanel} from '../../components/tracks/sql_table_slice_track_details_tab';
 import {Trace} from '../../public/trace';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {TrackEventSelection} from '../../public/selection';
+import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {escapeQuery} from '../../trace_processor/query_utils';
+import {Engine} from '../../trace_processor/engine';
+import {rows} from './utils';
+import {ColorScheme} from '../../base/color_scheme';
+import {JANK_COLOR} from './jank_colors';
+import {makeColorScheme} from '../../components/colorizer';
+import {HSLColor} from '../../base/color';
+import {ScrollTimelineDetailsPanel} from './scroll_timeline_details_panel';
 
 interface StepTemplate {
-  step_name: string;
-  ts_column_name: string;
-  dur_column_name: string;
+  // The name of a stage of a scroll.
+  // WARNING: This could be an arbitrary string so it MUST BE ESCAPED before
+  // using in an SQL query.
+  stepName: string;
+  // The name of the column in `chrome_scroll_update_info` which contains the
+  // timestamp of the step. If not null, this is guaranteed to be a valid column
+  // name, i.e. it's safe to use inline in an SQL query without any additional
+  // sanitization.
+  tsColumnName: string | null;
+  // The name of the column in `chrome_scroll_update_info` which contains the
+  // duration of the step. Null if the stage doesn't have a duration. If not
+  // null, this is guaranteed to be a valid column name, i.e. it's safe to use
+  // inline in an SQL query without any additional sanitization.
+  durColumnName: string | null;
 }
 
-// TODO: b/383547343 - Migrate STEP_TEMPLATES to a Chrome tracing stdlib table
-// once it's stable.
-const STEP_TEMPLATES: readonly StepTemplate[] = [
-  {
-    step_name: 'GenerationToBrowserMain',
-    ts_column_name: 'generation_ts',
-    dur_column_name: 'generation_to_browser_main_dur',
-  },
-  {
-    step_name: 'TouchMoveProcessing',
-    ts_column_name: 'touch_move_received_ts',
-    dur_column_name: 'touch_move_processing_dur',
-  },
-  {
-    step_name: 'ScrollUpdateProcessing',
-    ts_column_name: 'scroll_update_created_ts',
-    dur_column_name: 'scroll_update_processing_dur',
-  },
-  {
-    step_name: 'BrowserMainToRendererCompositor',
-    ts_column_name: 'scroll_update_created_end_ts',
-    dur_column_name: 'browser_to_compositor_delay_dur',
-  },
-  {
-    step_name: 'RendererCompositorDispatch',
-    ts_column_name: 'compositor_dispatch_ts',
-    dur_column_name: 'compositor_dispatch_dur',
-  },
-  {
-    step_name: 'RendererCompositorDispatchToOnBeginFrame',
-    ts_column_name: 'compositor_dispatch_end_ts',
-    dur_column_name: 'compositor_dispatch_to_on_begin_frame_delay_dur',
-  },
-  {
-    step_name: 'RendererCompositorBeginFrame',
-    ts_column_name: 'compositor_on_begin_frame_ts',
-    dur_column_name: 'compositor_on_begin_frame_dur',
-  },
-  {
-    step_name: 'RendererCompositorBeginToGenerateFrame',
-    ts_column_name: 'compositor_on_begin_frame_end_ts',
-    dur_column_name: 'compositor_on_begin_frame_to_generation_delay_dur',
-  },
-  {
-    step_name: 'RendererCompositorGenerateToSubmitFrame',
-    ts_column_name: 'compositor_generate_compositor_frame_ts',
-    dur_column_name: 'compositor_generate_frame_to_submit_frame_dur',
-  },
-  {
-    step_name: 'RendererCompositorSubmitFrame',
-    ts_column_name: 'compositor_submit_compositor_frame_ts',
-    dur_column_name: 'compositor_submit_frame_dur',
-  },
-  {
-    step_name: 'RendererCompositorToViz',
-    ts_column_name: 'compositor_submit_compositor_frame_end_ts',
-    dur_column_name: 'compositor_to_viz_delay_dur',
-  },
-  {
-    step_name: 'VizReceiveFrame',
-    ts_column_name: 'viz_receive_compositor_frame_ts',
-    dur_column_name: 'viz_receive_compositor_frame_dur',
-  },
-  {
-    step_name: 'VizReceiveToDrawFrame',
-    ts_column_name: 'viz_receive_compositor_frame_end_ts',
-    dur_column_name: 'viz_wait_for_draw_dur',
-  },
-  {
-    step_name: 'VizDrawToSwapFrame',
-    ts_column_name: 'viz_draw_and_swap_ts',
-    dur_column_name: 'viz_draw_and_swap_dur',
-  },
-  {
-    step_name: 'VizToGpu',
-    ts_column_name: 'viz_send_buffer_swap_end_ts',
-    dur_column_name: 'viz_to_gpu_delay_dur',
-  },
-  {
-    step_name: 'VizSwapBuffers',
-    ts_column_name: 'viz_swap_buffers_ts',
-    dur_column_name: 'viz_swap_buffers_dur',
-  },
-  {
-    step_name: 'VizSwapBuffersToLatch',
-    ts_column_name: 'viz_swap_buffers_end_ts',
-    dur_column_name: 'viz_swap_buffers_to_latch_dur',
-  },
-  {
-    step_name: 'VizLatchToSwapEnd',
-    ts_column_name: 'latch_timestamp',
-    dur_column_name: 'viz_latch_to_swap_end_dur',
-  },
-  {
-    step_name: 'VizSwapEndToPresentation',
-    ts_column_name: 'swap_end_timestamp',
-    dur_column_name: 'swap_end_to_presentation_dur',
-  },
-  {
-    // An artificial step to ensure that presentation_timestamp is included in
-    // the calculation of scroll_update_bounds. It's filtered out in
-    // unordered_slices due to NULL duration.
-    step_name: '',
-    ts_column_name: 'presentation_timestamp',
-    dur_column_name: 'NULL',
-  },
-];
+/**
+ * Classification of a scroll update for the purposes of trace visualization.
+ *
+ * If a scroll update matches multiple classifications (e.g. janky and
+ * inertial), it should be classified with the highest-priority one (e.g.
+ * janky). With the exception of `DEFAULT` and `STEP`, the values are sorted in
+ * the order of descending priority (i.e. `JANKY` has the highest priority).
+ */
+enum ScrollUpdateClassification {
+  // None of the other classifications apply.
+  DEFAULT = 0,
 
-export class ScrollTimelineTrack extends NamedSliceTrack<Slice, NamedRow> {
-  private readonly tableName;
+  // The corresponding frame was janky.
+  // See `chrome_scroll_update_input_info.is_janky`.
+  JANKY = 1,
 
-  constructor(trace: Trace, uri: string) {
-    super(trace, uri);
-    this.tableName = `scrolltimelinetrack_${sqlNameSafe(uri)}`;
+  // The input was coalesced into an earlier input's frame.
+  // See `chrome_scroll_update_input_info.is_first_scroll_update_in_frame`.
+  COALESCED = 2,
+
+  // It's the first scroll update in a scroll.
+  // Note: A first scroll update can never be janky.
+  // See `chrome_scroll_update_input_info.is_first_scroll_update_in_scroll`.
+  FIRST_SCROLL_UPDATE_IN_FRAME = 3,
+
+  // The corresponding scroll was inertial (i.e. a fling).
+  INERTIAL = 4,
+
+  // Sentinel value for slices which represent sub-steps of a scroll update.
+  STEP = -1,
+}
+
+const INDIGO = makeColorScheme(new HSLColor([231, 48, 48]));
+const GRAY = makeColorScheme(new HSLColor([0, 0, 62]));
+const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34]));
+const TEAL = makeColorScheme(new HSLColor([187, 90, 42]));
+
+function toColorScheme(
+  classification: ScrollUpdateClassification,
+): ColorScheme | undefined {
+  switch (classification) {
+    case ScrollUpdateClassification.DEFAULT:
+      return INDIGO;
+    case ScrollUpdateClassification.JANKY:
+      return JANK_COLOR;
+    case ScrollUpdateClassification.COALESCED:
+      return GRAY;
+    case ScrollUpdateClassification.FIRST_SCROLL_UPDATE_IN_FRAME:
+      return DARK_GREEN;
+    case ScrollUpdateClassification.INERTIAL:
+      return TEAL;
+    case ScrollUpdateClassification.STEP:
+      return undefined;
   }
-  override async onInit(): Promise<AsyncDisposable> {
-    await super.onInit();
-    await this.engine.query(`INCLUDE PERFETTO MODULE chrome.chrome_scrolls;`);
+}
+
+/**
+ * If `allowedColumnNames` contains `columnName`, returns `columnName`.
+ * Otherwise, returns null.
+ */
+function checkColumnNameIsValidOrReturnNull(
+  columnName: string | null,
+  allowedColumnNames: Set<string>,
+  errorMessagePrefix: string,
+): string | null {
+  if (columnName == null || allowedColumnNames.has(columnName)) {
+    return columnName;
+  } else {
+    console.error(
+      `${errorMessagePrefix}: ${columnName}
+      (allowed column names: ${Array.from(allowedColumnNames).join(', ')})`,
+    );
+    return null;
+  }
+}
+
+const SCROLL_TIMELINE_TRACK_ROW = {
+  ...NAMED_ROW,
+  classification: NUM,
+};
+type ScrollTimelineTrackRow = typeof SCROLL_TIMELINE_TRACK_ROW;
+
+export class ScrollTimelineTrack extends NamedSliceTrack<
+  Slice,
+  ScrollTimelineTrackRow
+> {
+  /**
+   * Constructs a scroll timeline track for a given `trace`.
+   *
+   * @param trace - The trace whose data the track will display
+   * @param uri - The URI of the track
+   * @param tableName - The name of an existing SQL table which contains
+   * information about the slices of the track. IMPORTANT: You must create
+   * the table first using {@link ScrollTimelineTrack.createTableForTrack}
+   * BEFORE creating this track.
+   */
+  constructor(
+    trace: Trace,
+    uri: string,
+    private readonly tableName: string,
+  ) {
+    super(trace, uri);
+  }
+
+  override getSqlSource(): string {
+    return `SELECT * FROM ${this.tableName}`;
+  }
+
+  override getRowSpec(): ScrollTimelineTrackRow {
+    return SCROLL_TIMELINE_TRACK_ROW;
+  }
+
+  override rowToSlice(row: ScrollTimelineTrackRow): Slice {
+    const baseSlice = super.rowToSliceBase(row);
+    const colorScheme = toColorScheme(row.classification);
+    if (colorScheme === undefined) {
+      return baseSlice;
+    } else {
+      return {...baseSlice, colorScheme};
+    }
+  }
+
+  override detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
+    return new ScrollTimelineDetailsPanel(
+      this.trace,
+      this.tableName,
+      sel.eventId,
+    );
+  }
+
+  /**
+   * Creates a Perfetto table named `tableName` representing the slices of a
+   * {@link ScrollTimelineTrack} for a given `trace`. You can use this table to
+   * construct the track.
+   */
+  static async createTableForTrack(
+    trace: Trace,
+    tableName: string,
+  ): Promise<void> {
+    const engine = trace.engine;
+    const stepTemplates = await ScrollTimelineTrack.queryStepTemplates(engine);
     // TODO: b/383549233 - Set ts+dur of each scroll update directly based on
     // our knowledge of the scrolling pipeline (as opposed to aggregating over
     // scroll_steps).
-    return await createPerfettoTable(
-      this.engine,
-      this.tableName,
-      `WITH
+    await engine.query(
+      `INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+      CREATE PERFETTO TABLE ${tableName} AS
+      WITH
         -- Unpivot all ts+dur columns into rows. Each row corresponds to a step
         -- of a particular scroll update. Some of the rows might have null
         -- ts/dur values, which will be filtered out in unordered_slices.
-        -- |scroll_steps| = |chrome_scroll_update_info| * |STEP_TEMPLATES|
-        scroll_steps AS (${STEP_TEMPLATES.map(
-          (step) => `
-          SELECT
-            id AS scroll_id,
-            ${step.ts_column_name} AS ts,
-            ${step.dur_column_name} AS dur,
-            '${step.step_name}' AS name
-          FROM chrome_scroll_update_info`,
-        ).join(' UNION ALL ')}),
+        -- |scroll_steps| = |chrome_scroll_update_info| * |stepTemplates|
+        scroll_steps AS (${stepTemplates
+          .map(
+            (step) => `
+              SELECT
+                id AS scroll_update_id,
+                ${step.tsColumnName ?? 'NULL'} AS ts,
+                ${step.durColumnName ?? 'NULL'} AS dur,
+                ${escapeQuery(step.stepName)} AS name
+              FROM chrome_scroll_update_info`,
+          )
+          .join(' UNION ALL ')}),
         -- For each scroll update, find its ts+dur by aggregating over all steps
         -- within the scroll update. We're basically trying to find MIN(COL1_ts,
         -- COL2_ts, ..., COLn_ts) and MAX(COL1_ts, COL2_ts, ..., COLn_ts) from
@@ -179,24 +220,61 @@
         -- values) than the scalar MIN/MAX functions (which return null if any
         -- argument is null). That's why we do it in such a roundabout way by
         -- joining the top-level table with the individual steps.
-        scroll_update_bounds AS (
+        scroll_updates_with_bounds AS (
           SELECT
-            scroll_update.id AS scroll_id,
+            scroll_update.id AS scroll_update_id,
             MIN(scroll_steps.ts) AS ts,
-            MAX(scroll_steps.ts) - MIN(scroll_steps.ts) AS dur
+            MAX(scroll_steps.ts) - MIN(scroll_steps.ts) AS dur,
+            -- Combine all applicable scroll update classifications into the
+            -- name. For example, if a scroll update is both janky and inertial,
+            -- its name will be name 'Janky Inertial Scroll Update'.
+            CONCAT_WS(
+              ' ',
+              IIF(scroll_update.is_janky, 'Janky', NULL),
+              IIF(scroll_update.is_first_scroll_update_in_scroll, 'First', NULL),
+              IIF(
+                NOT scroll_update.is_first_scroll_update_in_frame,
+                'Coalesced',
+                NULL
+              ),
+              IIF(scroll_update.is_inertial, 'Inertial', NULL),
+              'Scroll Update'
+            ) AS name,
+            -- Pick the highest-priority applicable scroll update
+            -- classification. For example, if a scroll update is both janky and
+            -- inertial, classify it as janky.
+            CASE
+              WHEN scroll_update.is_janky
+                THEN ${ScrollUpdateClassification.JANKY}
+              WHEN scroll_update.is_first_scroll_update_in_scroll
+                THEN ${ScrollUpdateClassification.FIRST_SCROLL_UPDATE_IN_FRAME}
+              WHEN NOT scroll_update.is_first_scroll_update_in_frame
+                THEN ${ScrollUpdateClassification.COALESCED}
+              WHEN scroll_update.is_inertial
+                THEN ${ScrollUpdateClassification.INERTIAL}
+              ELSE ${ScrollUpdateClassification.DEFAULT}
+            END AS classification
           FROM
             chrome_scroll_update_info AS scroll_update
-            JOIN scroll_steps ON scroll_steps.scroll_id = scroll_update.id
+            JOIN scroll_steps ON scroll_steps.scroll_update_id = scroll_update.id
           GROUP BY scroll_update.id
         ),
         -- Now that we know the ts+dur of all scroll updates, we can lay them
         -- out efficiently (i.e. assign depths to them to avoid overlaps).
-        scroll_update_layouts AS (
+        scroll_updates_with_layouts AS (
           ${generateSqlWithInternalLayout({
-            columns: ['scroll_id', 'ts', 'dur'],
-            sourceTable: 'scroll_update_bounds',
+            columns: [
+              'scroll_update_id',
+              'ts',
+              'dur',
+              'name',
+              'classification',
+            ],
+            sourceTable: 'scroll_updates_with_bounds',
             ts: 'ts',
             dur: 'dur',
+            // Filter out scroll updates with no timestamps. See b/388756942.
+            whereClause: 'ts IS NOT NULL AND dur IS NOT NULL',
           })}
         ),
         -- We interleave the top-level scroll update slices (at even depths) and
@@ -206,16 +284,20 @@
             ts,
             dur,
             2 * depth AS depth,
-            'Scroll Update' AS name
-          FROM scroll_update_layouts
+            name,
+            classification,
+            scroll_update_id
+          FROM scroll_updates_with_layouts
           UNION ALL
           SELECT
             scroll_steps.ts,
             MAX(scroll_steps.dur, 0) AS dur,
-            2 * scroll_update_layouts.depth + 1 AS depth,
-            scroll_steps.name
+            2 * scroll_updates_with_layouts.depth + 1 AS depth,
+            scroll_steps.name,
+            ${ScrollUpdateClassification.STEP} AS classification,
+            scroll_updates_with_layouts.scroll_update_id
           FROM scroll_steps
-          JOIN scroll_update_layouts USING(scroll_id)
+          JOIN scroll_updates_with_layouts USING(scroll_update_id)
           WHERE scroll_steps.ts IS NOT NULL AND scroll_steps.dur IS NOT NULL
         )
       -- Finally, we sort all slices chronologically and assign them
@@ -230,23 +312,64 @@
     );
   }
 
-  override getSqlSource(): string {
-    return `SELECT * FROM ${this.tableName}`;
-  }
-
-  override getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-
-  override rowToSlice(row: NamedRow): Slice {
-    return super.rowToSliceBase(row);
-  }
-
-  override detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel {
-    return new SqlTableSliceTrackDetailsPanel(
-      this.trace,
-      this.tableName,
-      sel.eventId,
+  /**
+   * Queries scroll step templates from
+   * `chrome_scroll_update_info_step_templates`.
+   *
+   * This function sanitizes the column names `StepTemplate.ts_column_name` and
+   * `StepTemplate.dur_column_name`. Unless null, the returned column names are
+   * guaranteed to be valid column names of `chrome_scroll_update_info`.
+   */
+  private static async queryStepTemplates(
+    engine: Engine,
+  ): Promise<StepTemplate[]> {
+    // Use a set for faster lookups.
+    const columnNames = new Set(
+      await ScrollTimelineTrack.queryChromeScrollUpdateInfoColumnNames(engine),
     );
+    const stepTemplatesResult = await engine.query(`
+      INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+      SELECT
+        step_name,
+        ts_column_name,
+        dur_column_name
+      FROM chrome_scroll_update_info_step_templates;`);
+    return rows(stepTemplatesResult, {
+      step_name: STR,
+      ts_column_name: STR_NULL,
+      dur_column_name: STR_NULL,
+    }).map(
+      // We defensively verify that the column names actually exist in the
+      // `chrome_scroll_update_info` table. We do this because we cannot update
+      // the `chrome_scroll_update_info` table and this plugin atomically
+      // (`chrome_scroll_update_info` is a part of the Chrome tracing stdlib,
+      // whose source of truth is in the Chromium repository).
+      (row) => ({
+        stepName: row.step_name,
+        tsColumnName: checkColumnNameIsValidOrReturnNull(
+          row.ts_column_name,
+          columnNames,
+          'Invalid ts_column_name in chrome_scroll_update_info_step_templates',
+        ),
+        durColumnName: checkColumnNameIsValidOrReturnNull(
+          row.dur_column_name,
+          columnNames,
+          'Invalid dur_column_name in chrome_scroll_update_info_step_templates',
+        ),
+      }),
+    );
+  }
+
+  /** Returns the names of columns of the `chrome_scroll_update_info` table. */
+  private static async queryChromeScrollUpdateInfoColumnNames(
+    engine: Engine,
+  ): Promise<string[]> {
+    // See https://www.sqlite.org/pragma.html#pragfunc and
+    // https://www.sqlite.org/pragma.html#pragma_table_info for more information
+    // about `pragma_table_info`.
+    const columnNamesResult = await engine.query(`
+      INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+      SELECT name FROM pragma_table_info('chrome_scroll_update_info');`);
+    return rows(columnNamesResult, {name: STR}).map((row) => row.name);
   }
 }
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
deleted file mode 100644
index 4b79e05..0000000
--- a/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
+++ /dev/null
@@ -1,42 +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.
-
-import m from 'mithril';
-import {Anchor} from '../../widgets/anchor';
-import {Icons} from '../../base/semantic_icons';
-import {Trace} from '../../public/trace';
-
-export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
-export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
-export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
-
-export function renderSliceRef(args: {
-  trace: Trace;
-  id: number;
-  trackUri: string;
-  title: m.Children;
-}) {
-  return m(
-    Anchor,
-    {
-      icon: Icons.UpdateSelection,
-      onclick: () => {
-        args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
-          scrollToSelection: true,
-        });
-      },
-    },
-    args.title,
-  );
-}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts
new file mode 100644
index 0000000..1801336
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/utils.ts
@@ -0,0 +1,77 @@
+// 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 {Anchor} from '../../widgets/anchor';
+import {Icons} from '../../base/semantic_icons';
+import {Trace} from '../../public/trace';
+import {QueryResult, Row} from '../../trace_processor/query_result';
+
+export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
+export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
+export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
+
+export function renderSliceRef(args: {
+  trace: Trace;
+  id: number;
+  trackUri: string;
+  title: m.Children;
+}) {
+  return m(
+    Anchor,
+    {
+      icon: Icons.UpdateSelection,
+      onclick: () => {
+        args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
+          scrollToSelection: true,
+        });
+      },
+    },
+    args.title,
+  );
+}
+
+/**
+ * Returns an array of the rows in `queryResult`.
+ *
+ * Warning: Only use this function in contexts where the number of rows is
+ * guaranteed to be small. Prefer doing transformations in SQL where possible.
+ */
+export function rows<R extends Row>(queryResult: QueryResult, spec: R): R[] {
+  const results: R[] = [];
+  for (const it = queryResult.iter(spec); it.valid(); it.next()) {
+    const row: Row = {};
+    for (const key of Object.keys(spec)) {
+      row[key] = it[key];
+    }
+    results.push(row as R);
+  }
+  return results;
+}
+
+/**
+ * Converts a number to a boolean according to SQLite's conversion rules.
+ *
+ * See https://www.sqlite.org/lang_expr.html#boolean_expressions.
+ */
+export function fromSqlBool(value: number): boolean;
+export function fromSqlBool(value: null): undefined;
+export function fromSqlBool(value: number | null): boolean | undefined;
+export function fromSqlBool(value: number | null): boolean | undefined {
+  if (value === null) {
+    return undefined;
+  } else {
+    return value !== 0;
+  }
+}
diff --git a/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts
new file mode 100644
index 0000000..90f46b7
--- /dev/null
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/utils_unittest.ts
@@ -0,0 +1,76 @@
+// Copyright (C) 2025 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 protos from '../../protos';
+import {
+  createQueryResult,
+  LONG_NULL,
+  NUM,
+  STR,
+} from '../../trace_processor/query_result';
+import {fromSqlBool, rows} from './utils';
+
+const T = protos.QueryResult.CellsBatch.CellType;
+
+test('rows', () => {
+  // Manually construct a QueryResult which is equivalent to running the
+  // following SQL query:
+  //
+  // SELECT
+  //   column1,
+  //   column2,
+  //   column3
+  // FROM (
+  //   VALUES
+  //   ('A', 10, 100),
+  //   ('B', 20, 200),
+  //   ('C', 30, NULL)
+  // )
+  // ORDER BY column1 ASC
+  const batch = protos.QueryResult.CellsBatch.create({
+    cells: [
+      [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_VARINT],
+      [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_VARINT],
+      [T.CELL_STRING, T.CELL_FLOAT64, T.CELL_NULL],
+    ].flat(),
+    stringCells: ['A', 'B', 'C'].join('\0'),
+    float64Cells: [10, 20, 30],
+    varintCells: [100, 200],
+    isLastBatch: true,
+  });
+  const resultProto = protos.QueryResult.create({
+    columnNames: ['column1', 'column2', 'column3'],
+    batch: [batch],
+  });
+  const queryResult = createQueryResult({query: 'Some query'});
+  queryResult.appendResultBatch(
+    protos.QueryResult.encode(resultProto).finish(),
+  );
+
+  expect(
+    rows(queryResult, {column1: STR, column2: NUM, column3: LONG_NULL}),
+  ).toStrictEqual([
+    {column1: 'A', column2: 10, column3: 100n},
+    {column1: 'B', column2: 20, column3: 200n},
+    {column1: 'C', column2: 30, column3: null},
+  ]);
+});
+
+test('fromSqlBool', () => {
+  expect(fromSqlBool(null)).toBeUndefined();
+  expect(fromSqlBool(-1)).toStrictEqual(true);
+  expect(fromSqlBool(0)).toStrictEqual(false);
+  expect(fromSqlBool(0.1)).toStrictEqual(true);
+  expect(fromSqlBool(1)).toStrictEqual(true);
+});
diff --git a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
index b43f249..1cd149c 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
@@ -15,15 +15,15 @@
 import {NUM, STR_NULL} from '../../trace_processor/query_result';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
-import {AsyncSliceTrack} from '../dev.perfetto.AsyncSlices/async_slice_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
 import {SLICE_TRACK_KIND} from '../../public/track_kinds';
 import {TrackNode} from '../../public/workspace';
-import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
 
 // This plugin renders visualizations of subsystems of the Linux kernel.
 export default class implements PerfettoPlugin {
   static readonly id = 'org.kernel.LinuxKernelSubsystems';
-  static readonly dependencies = [AsyncSlicesPlugin];
+  static readonly dependencies = [TraceProcessorTrackPlugin];
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     const kernel = new TrackNode({
@@ -66,7 +66,7 @@
       ctx.tracks.registerTrack({
         uri,
         title,
-        track: new AsyncSliceTrack(ctx, uri, 0, [trackId]),
+        track: new TraceProcessorSliceTrack(ctx, uri, 0, [trackId]),
         tags: {
           kind: SLICE_TRACK_KIND,
           trackIds: [trackId],
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
index ca1e88c..caf75d1 100644
--- a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {NUM, STR_NULL} from '../../trace_processor/query_result';
-import {AsyncSliceTrack} from '../dev.perfetto.AsyncSlices/async_slice_track';
+import {TraceProcessorSliceTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_slice_track';
 import {PerfettoPlugin} from '../../public/plugin';
 import {Trace} from '../../public/trace';
 import {TrackNode} from '../../public/workspace';
@@ -21,12 +21,12 @@
 import {SuspendResumeDetailsPanel} from './suspend_resume_details';
 import {ThreadMap} from '../dev.perfetto.Thread/threads';
 import ThreadPlugin from '../dev.perfetto.Thread';
-import AsyncSlicesPlugin from '../dev.perfetto.AsyncSlices';
+import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
 
 // SuspendResumeSliceTrack exists so as to override the `onSliceClick` function
 // in AsyncSliceTrack.
 // TODO(stevegolton): Remove this?
-class SuspendResumeSliceTrack extends AsyncSliceTrack {
+class SuspendResumeSliceTrack extends TraceProcessorSliceTrack {
   constructor(
     trace: Trace,
     uri: string,
@@ -44,7 +44,7 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'org.kernel.SuspendResumeLatency';
-  static readonly dependencies = [ThreadPlugin, AsyncSlicesPlugin];
+  static readonly dependencies = [ThreadPlugin, TraceProcessorTrackPlugin];
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     const threads = ctx.plugins.getPlugin(ThreadPlugin).getThreadMap();
diff --git a/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
index 7b5a530..867c1c4 100644
--- a/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
+++ b/ui/src/plugins/org.kernel.Wattson/thread_aggregator.ts
@@ -55,7 +55,7 @@
 
       -- Only get idle attribution in user defined window and filter by selected
       -- CPUs and GROUP BY thread
-      CREATE OR REPLACE PERFETTO TABLE _per_thread_idle_attribution AS
+      CREATE OR REPLACE PERFETTO TABLE _per_thread_idle_cost AS
       SELECT
         ROUND(SUM(idle_cost_mws), 2) as idle_cost_mws,
         utid
@@ -141,7 +141,7 @@
         tid,
         pid
       FROM _unioned_per_cpu_total
-      LEFT JOIN _per_thread_idle_attribution USING (utid)
+      LEFT JOIN _per_thread_idle_cost USING (utid)
       GROUP BY utid;
     `;
 
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 0c8321b..03ad92e 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -21,6 +21,7 @@
 import {Trace} from './trace';
 import {PageManager} from './page';
 import {FeatureFlagManager} from './feature_flag';
+import {Raf} from './raf';
 
 /**
  * The API endpoint to interact programmaticaly with the UI before a trace has
@@ -52,9 +53,10 @@
    */
   readonly trace?: Trace;
 
-  // TODO(primiano): this should be needed in extremely rare cases. We should
-  // probably switch to mithril auto-redraw at some point.
-  scheduleFullRedraw(force?: 'force'): void;
+  /**
+   * Used to schedule things.
+   */
+  readonly raf: Raf;
 
   /**
    * Navigate to a new page.
diff --git a/ui/src/public/raf.ts b/ui/src/public/raf.ts
new file mode 100644
index 0000000..cfa1d8e
--- /dev/null
+++ b/ui/src/public/raf.ts
@@ -0,0 +1,36 @@
+// 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.
+
+export type RedrawCallback = () => void;
+
+export interface Raf {
+  /**
+   * Schedule both a DOM and canvas redraw.
+   */
+  scheduleFullRedraw(): void;
+
+  /**
+   * Schedule a canvas redraw only.
+   */
+  scheduleCanvasRedraw(): void;
+
+  /**
+   * Add a callback for canvas redraws. `cb` will be called whenever a canvas
+   * redraw is scheduled canvas redraw using {@link scheduleCanvasRedraw()}.
+   *
+   * @param cb - The callback to called when canvas are redrawn.
+   * @returns - A disposable object that removes the callback when disposed.
+   */
+  addCanvasRedrawCallback(cb: RedrawCallback): Disposable;
+}
diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts
index 945b398..5f1e9dc 100644
--- a/ui/src/public/selection.ts
+++ b/ui/src/public/selection.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {time, duration, TimeSpan} from '../base/time';
+import {Dataset, DatasetSchema} from '../trace_processor/dataset';
 import {Engine} from '../trace_processor/engine';
 import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation';
 import {TrackDescriptor} from './track';
@@ -74,12 +75,51 @@
   registerSqlSelectionResolver(resolver: SqlSelectionResolver): void;
 }
 
+/**
+ * Aggregator tabs are displayed in descending order of specificity, determined
+ * by the following precedence hierarchy:
+ * 1. Aggregators explicitly defining a `trackKind` string take priority over
+ *    those that do not.
+ * 2. Otherwise, aggregators with schemas containing a greater number of keys
+ *    (higher specificity) are prioritized over those with fewer keys.
+ * 3. In cases of identical specificity, tabs are ranked based on their
+ *    registration order.
+ */
 export interface AreaSelectionAggregator {
   readonly id: string;
-  createAggregateView(engine: Engine, area: AreaSelection): Promise<boolean>;
+
+  /**
+   * If defined, the dataset passed to `createAggregateView` will only contain
+   * tracks with a matching `kind` tag.
+   */
+  readonly trackKind?: string;
+
+  /**
+   * If defined, the dataset passed to `createAggregateView` will only contain
+   * tracks that export datasets that implement this schema.
+   */
+  readonly schema?: DatasetSchema;
+
+  /**
+   * Creates a view for the aggregated data corresponding to the selected area.
+   *
+   * The dataset provided will be filtered based on the `trackKind` and `schema`
+   * if these properties are defined.
+   *
+   * @param engine - The query engine used to execute queries.
+   * @param area - The currently selected area to aggregate.
+   * @param dataset - The dataset representing a union of the data in the
+   * selected tracks.
+   */
+  createAggregateView(
+    engine: Engine,
+    area: AreaSelection,
+    dataset?: Dataset,
+  ): Promise<boolean>;
   getExtra(
     engine: Engine,
     area: AreaSelection,
+    dataset?: Dataset,
   ): Promise<ThreadStateExtra | void>;
   getTabName(): string;
   getDefaultSorting(): Sorting;
@@ -162,6 +202,7 @@
   JAVA_HEAP_SAMPLES = 'heap_profile:com.android.art',
   JAVA_HEAP_GRAPH = 'graph',
   PERF_SAMPLE = 'perf',
+  INSTRUMENTS_SAMPLE = 'instruments',
 }
 
 export function profileType(s: string): ProfileType {
diff --git a/ui/src/public/standard_groups.ts b/ui/src/public/standard_groups.ts
deleted file mode 100644
index 2bc7570..0000000
--- a/ui/src/public/standard_groups.ts
+++ /dev/null
@@ -1,47 +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.
-
-import {TrackNode, TrackNodeArgs, Workspace} from './workspace';
-
-/**
- * Gets or creates a group for user interaction
- *
- * @param workspace - The workspace on which to create the group.
- */
-export function getOrCreateUserInteractionGroup(
-  workspace: Workspace,
-): TrackNode {
-  return getOrCreateGroup(workspace, 'user_interaction', {
-    title: 'User Interaction',
-    collapsed: false, // Expand this by default
-    isSummary: true,
-  });
-}
-
-// Internal utility function to avoid duplicating the logic to get or create a
-// group by ID.
-function getOrCreateGroup(
-  workspace: Workspace,
-  id: string,
-  args?: Omit<Partial<TrackNodeArgs>, 'id'>,
-): TrackNode {
-  const group = workspace.getTrackById(id);
-  if (group) {
-    return group;
-  } else {
-    const group = new TrackNode({id, ...args});
-    workspace.addChildInOrder(group);
-    return group;
-  }
-}
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 6d1b1dc..b10d4e0 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -17,7 +17,7 @@
 import {Size2D, VerticalBounds} from '../base/geom';
 import {TimeScale} from '../base/time_scale';
 import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
-import {ColorScheme} from './color_scheme';
+import {ColorScheme} from '../base/color_scheme';
 import {TrackEventDetailsPanel} from './details_panel';
 import {TrackEventDetails, TrackEventSelection} from './selection';
 import {Dataset} from '../trace_processor/dataset';
diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts
index d01e43e..3c31edf 100644
--- a/ui/src/public/track_kinds.ts
+++ b/ui/src/public/track_kinds.ts
@@ -22,6 +22,8 @@
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
 export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
+export const INSTRUMENTS_SAMPLES_PROFILE_TRACK_KIND =
+  'InstrumentsSamplesProfileTrack';
 export const COUNTER_TRACK_KIND = 'CounterTrack';
 export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
 export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 657bed6..a5f9b1b 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -73,14 +73,14 @@
     return `${name} ${uid}`;
   } else if (hasName) {
     return `${name}`;
-  } else if (hasUpid && hasPid && hasProcessName) {
-    return `${processName} ${pid}`;
-  } else if (hasUpid && hasPid) {
-    return `Process ${pid}`;
   } else if (hasThreadName && hasTid) {
     return `${threadName} ${tid}`;
   } else if (hasTid) {
     return `Thread ${tid}`;
+  } else if (hasUpid && hasPid && hasProcessName) {
+    return `${processName} ${pid}`;
+  } else if (hasUpid && hasPid) {
+    return `Process ${pid}`;
   } else if (hasUpid) {
     return `upid: ${upid}${kindSuffix}`;
   } else if (hasUtid) {
diff --git a/ui/src/public/workspace.ts b/ui/src/public/workspace.ts
index 013771d..3c3955b 100644
--- a/ui/src/public/workspace.ts
+++ b/ui/src/public/workspace.ts
@@ -443,7 +443,7 @@
   }
 
   /**
-   * Find a track node by its id.
+   * Get a track node by its id.
    *
    * Node: This is an O(1) operation.
    *
@@ -455,14 +455,14 @@
   }
 
   /**
-   * Find a track node via its URI.
+   * Get a track node via its URI.
    *
    * Node: This is an O(1) operation.
    *
    * @param uri The uri of the track to find.
    * @returns The node or undefined if no such node exists with this URI.
    */
-  findTrackByUri(uri: string): TrackNode | undefined {
+  getTrackByUri(uri: string): TrackNode | undefined {
     return this.tracksByUri.get(uri);
   }
 
@@ -597,21 +597,24 @@
   }
 
   /**
-   * Find a track node via its URI.
+   * Get a track node via its URI.
    *
-   * Note: This in an O(N) operation where N is the number of nodes in the
-   * workspace.
+   * Node: This is an O(1) operation.
    *
    * @param uri The uri of the track to find.
-   * @returns A reference to the track node if it exists in this workspace,
-   * otherwise undefined.
+   * @returns The node or undefined if no such node exists with this URI.
    */
-  findTrackByUri(uri: string): TrackNode | undefined {
+  getTrackByUri(uri: string): TrackNode | undefined {
     return this.tracks.flatTracks.find((t) => t.uri === uri);
   }
 
   /**
-   * Find a track by ID, also searching pinned tracks.
+   * Get a track node by its id.
+   *
+   * Node: This is an O(1) operation.
+   *
+   * @param id The id of the node we want to find.
+   * @returns The node or undefined if no such node exists.
    */
   getTrackById(id: string): TrackNode | undefined {
     return (
diff --git a/ui/src/public/workspace_unittest.ts b/ui/src/public/workspace_unittest.ts
index 333496a..215510e 100644
--- a/ui/src/public/workspace_unittest.ts
+++ b/ui/src/public/workspace_unittest.ts
@@ -64,7 +64,7 @@
     expect(workspace.getTrackById('bar')).toBe(undefined);
   });
 
-  test('findTrackByUri()', () => {
+  test('getTrackByUri()', () => {
     const workspace = new Workspace();
 
     const group = new TrackNode();
@@ -75,7 +75,7 @@
     // Add group to workspace
     workspace.addChildLast(group);
 
-    expect(workspace.findTrackByUri('foo')).toBe(track);
+    expect(workspace.getTrackByUri('foo')).toBe(track);
   });
 
   test('findClosestVisibleAncestor()', () => {
diff --git a/ui/src/test/aggregation.test.ts b/ui/src/test/aggregation.test.ts
index 449d23b..3913c48 100644
--- a/ui/src/test/aggregation.test.ts
+++ b/ui/src/test/aggregation.test.ts
@@ -64,7 +64,7 @@
 
 test('frametimeline', async () => {
   await page.keyboard.press('Escape');
-  const sysui = pth.locateTrackGroup('com.android.systemui 25348');
+  const sysui = pth.locateTrack('com.android.systemui 25348');
   await sysui.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(sysui);
   const actualTimeline = pth.locateTrack(
@@ -81,7 +81,7 @@
 
 test('slices', async () => {
   await page.keyboard.press('Escape');
-  const syssrv = pth.locateTrackGroup('system_server 1719');
+  const syssrv = pth.locateTrack('system_server 1719');
   await syssrv.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(syssrv);
   const animThread = pth
diff --git a/ui/src/test/chrome_missing_track_names.test.ts b/ui/src/test/chrome_missing_track_names.test.ts
index 3f20bc0..54d7652 100644
--- a/ui/src/test/chrome_missing_track_names.test.ts
+++ b/ui/src/test/chrome_missing_track_names.test.ts
@@ -31,6 +31,6 @@
 });
 
 test('expand all tracks', async () => {
-  await page.click('.header-panel-container button[title="Expand all"]');
+  await page.click('.pf-viewer-page__header button[title="Expand all"]');
   await pth.waitForIdleAndScreenshot('all_tracks_expanded.png');
 });
diff --git a/ui/src/test/chrome_rendering_desktop.test.ts b/ui/src/test/chrome_rendering_desktop.test.ts
index 8cc57ee..f3212c2 100644
--- a/ui/src/test/chrome_rendering_desktop.test.ts
+++ b/ui/src/test/chrome_rendering_desktop.test.ts
@@ -31,7 +31,7 @@
 });
 
 test('expand browser', async () => {
-  const grp = pth.locateTrackGroup('Browser 12685');
+  const grp = pth.locateTrack('Browser 12685');
   grp.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(grp);
   await pth.waitForIdleAndScreenshot('browser_expanded.png');
diff --git a/ui/src/test/ftrace_tracks_and_tab.test.ts b/ui/src/test/ftrace_tracks_and_tab.test.ts
index 7bed34b..3c4303d 100644
--- a/ui/src/test/ftrace_tracks_and_tab.test.ts
+++ b/ui/src/test/ftrace_tracks_and_tab.test.ts
@@ -27,7 +27,8 @@
 });
 
 test('ftrace tracks', async () => {
-  await page.click('h1[ref="Ftrace Events"]');
+  const ftraceGroupTrack = pth.locateTrack('Ftrace Events');
+  await pth.toggleTrackGroup(ftraceGroupTrack);
   await pth.waitForIdleAndScreenshot('ftrace_events.png');
 });
 
diff --git a/ui/src/test/independent_features.test.ts b/ui/src/test/independent_features.test.ts
index c761ded..20f0c68 100644
--- a/ui/src/test/independent_features.test.ts
+++ b/ui/src/test/independent_features.test.ts
@@ -23,7 +23,7 @@
   const page = await browser.newPage();
   const pth = new PerfettoTestHelper(page);
   await pth.openTraceFile('api32_startup_warm.perfetto-trace');
-  const trackGroup = pth.locateTrackGroup(
+  const trackGroup = pth.locateTrack(
     'androidx.benchmark.integration.macrobenchmark.test 7527',
   );
   await trackGroup.scrollIntoViewIfNeeded();
diff --git a/ui/src/test/load_and_tracks.test.ts b/ui/src/test/load_and_tracks.test.ts
index f02611c..3aaa994 100644
--- a/ui/src/test/load_and_tracks.test.ts
+++ b/ui/src/test/load_and_tracks.test.ts
@@ -71,9 +71,9 @@
 });
 
 test('track expand and collapse', async () => {
-  const trackGroup = pth.locateTrackGroup('traced_probes 1054');
+  const trackGroup = pth.locateTrack('traced_probes 1054');
   await trackGroup.scrollIntoViewIfNeeded();
-  await trackGroup.click();
+  await pth.toggleTrackGroup(trackGroup);
   await pth.waitForIdleAndScreenshot('traced_probes_expanded.png');
 
   // Click 5 times in rapid succession.
@@ -85,7 +85,7 @@
 });
 
 test('pin tracks', async () => {
-  const trackGroup = pth.locateTrackGroup('traced 1055');
+  const trackGroup = pth.locateTrack('traced 1055');
   await pth.toggleTrackGroup(trackGroup);
   let track = pth.locateTrack('traced 1055/mem.rss', trackGroup);
   await pth.pinTrackUsingShellBtn(track);
diff --git a/ui/src/test/perfetto_ui_test_helper.ts b/ui/src/test/perfetto_ui_test_helper.ts
index 20e1f82..d2e5b16 100644
--- a/ui/src/test/perfetto_ui_test_helper.ts
+++ b/ui/src/test/perfetto_ui_test_helper.ts
@@ -82,21 +82,13 @@
     await expect.soft(this.page).toHaveScreenshot(screenshotName, opts);
   }
 
-  locateTrackGroup(name: string): Locator {
-    return this.page
-      .locator('.pf-panel-group')
-      .filter({has: this.page.locator(`h1[ref="${name}"]`)});
-  }
-
   async toggleTrackGroup(locator: Locator) {
-    await locator.locator('.pf-track-title').first().click();
+    await locator.locator('.pf-track__shell').first().click();
     await this.waitForPerfettoIdle();
   }
 
   locateTrack(name: string, trackGroup?: Locator): Locator {
-    return (trackGroup ?? this.page)
-      .locator('.pf-track')
-      .filter({has: this.page.locator(`h1[ref="${name}"]`)});
+    return (trackGroup ?? this.page).locator(`.pf-track[ref="${name}"]`);
   }
 
   pinTrackUsingShellBtn(track: Locator) {
diff --git a/ui/src/test/track_event_ordered_tracks.test.ts b/ui/src/test/track_event_ordered_tracks.test.ts
index 8f39a3a..1097e37 100644
--- a/ui/src/test/track_event_ordered_tracks.test.ts
+++ b/ui/src/test/track_event_ordered_tracks.test.ts
@@ -31,7 +31,7 @@
 });
 
 test('chronological order', async () => {
-  const chronologicalGrp = pth.locateTrackGroup('Root Chronological');
+  const chronologicalGrp = pth.locateTrack('Root Chronological');
   await chronologicalGrp.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(chronologicalGrp);
 
@@ -39,7 +39,7 @@
 });
 
 test('explicit order', async () => {
-  const explicitGrp = pth.locateTrackGroup('Root Explicit');
+  const explicitGrp = pth.locateTrack('Root Explicit');
   await explicitGrp.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(explicitGrp);
 
@@ -47,7 +47,7 @@
 });
 
 test('lexicographic tracks', async () => {
-  const lexicographicGrp = pth.locateTrackGroup('Root Lexicographic');
+  const lexicographicGrp = pth.locateTrack('Root Lexicographic');
   await lexicographicGrp.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(lexicographicGrp);
 
diff --git a/ui/src/test/wattson.test.ts b/ui/src/test/wattson.test.ts
index 9bff14c..6fe3ee3 100644
--- a/ui/src/test/wattson.test.ts
+++ b/ui/src/test/wattson.test.ts
@@ -40,7 +40,7 @@
 });
 
 test('wattson aggregations', async () => {
-  const wattsonGrp = pth.locateTrackGroup('Wattson');
+  const wattsonGrp = pth.locateTrack('Wattson');
   await wattsonGrp.scrollIntoViewIfNeeded();
   await pth.toggleTrackGroup(wattsonGrp);
   const cpuEstimate = pth.locateTrack('Wattson/Cpu0 Estimate', wattsonGrp);
diff --git a/ui/src/trace_processor/dataset.ts b/ui/src/trace_processor/dataset.ts
index 25c64cb..42441b2 100644
--- a/ui/src/trace_processor/dataset.ts
+++ b/ui/src/trace_processor/dataset.ts
@@ -104,7 +104,7 @@
  * Defines a list of columns and types that define the shape of the data
  * represented by a dataset.
  */
-export type DatasetSchema = Record<string, ColumnType>;
+export type DatasetSchema = Readonly<Record<string, ColumnType>>;
 
 /**
  * A filter used to express that a column must equal a value.
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 3d8541e..e959cbf 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -539,23 +539,25 @@
 export class EngineProxy implements Engine, Disposable {
   private engine: EngineBase;
   private tag: string;
-  private _isAlive: boolean;
+  private disposed = false;
 
   constructor(engine: EngineBase, tag: string) {
     this.engine = engine;
     this.tag = tag;
-    this._isAlive = true;
   }
 
   async query(query: string, tag?: string): Promise<QueryResult> {
-    if (!this._isAlive) {
-      throw new Error(`EngineProxy ${this.tag} was disposed.`);
+    if (this.disposed) {
+      // If we are disposed (the trace was closed), return an empty QueryResult
+      // that will never see any data or EOF. We can't do otherwise or it will
+      // cause crashes to code calling firstRow() and expecting data.
+      return createQueryResult({query});
     }
     return await this.engine.query(query, tag);
   }
 
   async tryQuery(query: string, tag?: string): Promise<Result<QueryResult>> {
-    if (!this._isAlive) {
+    if (this.disposed) {
       return errResult(`EngineProxy ${this.tag} was disposed`);
     }
     return await this.engine.tryQuery(query, tag);
@@ -565,8 +567,8 @@
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array> {
-    if (!this._isAlive) {
-      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
+    if (this.disposed) {
+      return defer<string>(); // Return a promise that will hang forever.
     }
     return this.engine.computeMetric(metrics, format);
   }
@@ -600,7 +602,7 @@
   }
 
   [Symbol.dispose]() {
-    this._isAlive = false;
+    this.disposed = true;
   }
 }
 
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index 6c87f27..b54efaf 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -126,6 +126,7 @@
 
   [Symbol.dispose]() {
     this.disposed = true;
+    this.connected = false;
     const websocket = this.websocket;
     this.websocket = undefined;
     websocket?.close();
diff --git a/ui/src/trace_processor/proto_ring_buffer.ts b/ui/src/trace_processor/proto_ring_buffer.ts
index 5dfdac5..495c348 100644
--- a/ui/src/trace_processor/proto_ring_buffer.ts
+++ b/ui/src/trace_processor/proto_ring_buffer.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue} from '../base/logging';
+import {assertTrue, assertUnreachable} from '../base/logging';
 
 // This class is the TypeScript equivalent of the identically-named C++ class in
 // //protozero/proto_ring_buffer.h. See comments in that header for a detailed
@@ -21,12 +21,23 @@
 const kGrowBytes = 128 * 1024;
 const kMaxMsgSize = 1024 * 1024 * 1024;
 
+// There are two ways the buffer can work:
+// 1. 'PROTO_PREAMBLE' is the case where the header before each packet is a
+//    length-encoded protobuf message. This is the case when paring traces or
+//    the TraceProcessor RPC protocol.
+// 2. 'FIXED_SIZE' is the case where the header is a 32-bit integer representing
+//    the size of the message. This is used by the traced tracing protocol in
+//    https://perfetto.dev/docs/design-docs/api-and-abi#socket-protocol.
+export type ProtoTokenizationMode = 'PROTO_PREAMBLE' | 'FIXED_SIZE';
+
 export class ProtoRingBuffer {
   private buf = new Uint8Array(kGrowBytes);
   private fastpath?: Uint8Array;
   private rd = 0;
   private wr = 0;
 
+  constructor(private mode: ProtoTokenizationMode = 'PROTO_PREAMBLE') {}
+
   // The caller must call ReadMessage() after each append() call.
   // The |data| might be either copied in the internal ring buffer or returned
   // (% subarray()) to the next ReadMessage() call.
@@ -46,7 +57,7 @@
     if (dataLen === 0) return;
     assertTrue(this.fastpath === undefined);
     if (this.rd === this.wr) {
-      const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+      const msg = this.tryReadMessage(data, 0, dataLen);
       if (
         msg !== undefined &&
         msg.byteOffset + msg.length === data.byteOffset + dataLen
@@ -107,7 +118,7 @@
     if (this.rd >= this.wr) {
       return undefined; // Completely empty.
     }
-    const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+    const msg = this.tryReadMessage(this.buf, this.rd, this.wr);
     if (msg === undefined) return undefined;
     assertTrue(msg.buffer === this.buf.buffer);
     assertTrue(this.buf.byteOffset === 0);
@@ -120,7 +131,7 @@
     return msg.slice();
   }
 
-  private static tryReadMessage(
+  private tryReadMessage(
     data: Uint8Array,
     dataStart: number,
     dataEnd: number,
@@ -128,21 +139,34 @@
     assertTrue(dataEnd <= data.length);
     let pos = dataStart;
     if (pos >= dataEnd) return undefined;
-    const tag = data[pos++]; // Assume one-byte tag.
-    if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
-      throw new Error(
-        `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`,
-      );
-    }
-
     let len = 0;
-    for (let shift = 0 /* no check */; ; shift += 7) {
-      if (pos >= dataEnd) {
-        return undefined; // Not enough data to read varint.
+
+    if (this.mode === 'PROTO_PREAMBLE') {
+      const tag = data[pos++]; // Assume one-byte tag.
+      if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+        throw new Error(
+          `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`,
+        );
       }
-      const val = data[pos++];
-      len |= ((val & 0x7f) << shift) >>> 0;
-      if (val < 0x80) break;
+
+      for (let shift = 0 /* no check */; ; shift += 7) {
+        if (pos >= dataEnd) {
+          return undefined; // Not enough data to read varint.
+        }
+        const val = data[pos++];
+        len |= ((val & 0x7f) << shift) >>> 0;
+        if (val < 0x80) break;
+      }
+    } else if (this.mode === 'FIXED_SIZE') {
+      for (let i = 0; i < 4; i++) {
+        if (pos >= dataEnd) {
+          return undefined; // Not enough data to read a uint32.
+        }
+        const val = data[pos++] & 0xff;
+        len |= (val << (i * 8)) >>> 0;
+      }
+    } else {
+      assertUnreachable(this.mode);
     }
 
     if (len >= kMaxMsgSize) {
diff --git a/ui/src/widgets/basic_table.ts b/ui/src/widgets/basic_table.ts
index 64490e5..9474895 100644
--- a/ui/src/widgets/basic_table.ts
+++ b/ui/src/widgets/basic_table.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {scheduleFullRedraw} from './raf';
 
 export interface ColumnDescriptor<T> {
   readonly title: m.Children;
@@ -161,8 +160,6 @@
             if (e.dataTransfer !== null) {
               e.dataTransfer.setDragImage(placeholderElement, 0, 0);
             }
-
-            scheduleFullRedraw();
           },
           ondragover: (e: DragEvent) => {
             let target = e.target as HTMLElement;
@@ -194,13 +191,11 @@
                 : dest;
             if (adjustedDest !== this.drag.to) {
               this.drag.to = adjustedDest;
-              scheduleFullRedraw();
             }
           },
           ondragleave: (e: DragEvent) => {
             if (this.drag?.to !== index) return;
             this.drag.to = undefined;
-            scheduleFullRedraw();
             if (e.dataTransfer !== null) {
               e.dataTransfer.dropEffect = 'none';
             }
@@ -215,7 +210,6 @@
             }
 
             this.drag = undefined;
-            scheduleFullRedraw();
           },
         },
         cell.content,
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 1187be0..59b48d2 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -21,7 +21,6 @@
 import {assertExists} from '../base/logging';
 import {DragGestureHandler} from '../base/drag_gesture_handler';
 import {DisposableStack} from '../base/disposable_stack';
-import {scheduleFullRedraw} from './raf';
 
 export interface EditorAttrs {
   // Initial state for the editor.
@@ -65,7 +64,7 @@
             text = selectedText;
           }
           onExecute(text);
-          scheduleFullRedraw('force');
+          m.redraw();
           return true;
         },
       });
@@ -77,7 +76,7 @@
         view.update([tr]);
         const text = view.state.doc.toString();
         onUpdate(text);
-        scheduleFullRedraw('force');
+        m.redraw();
       };
     }
 
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index 3c831dd..7a3b022 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -19,7 +19,6 @@
 import {Button, ButtonBar} from './button';
 import {EmptyState} from './empty_state';
 import {Popup, PopupPosition} from './popup';
-import {scheduleFullRedraw} from './raf';
 import {Select} from './select';
 import {Spinner} from './spinner';
 import {TagInput} from './tag_input';
@@ -248,7 +247,8 @@
       m(
         '.canvas-container[ref=canvas-container]',
         {
-          onscroll: () => scheduleFullRedraw(),
+          // This will trigger auto redraws
+          onscroll: () => {},
         },
         m(
           Popup,
@@ -271,7 +271,6 @@
         m(`canvas[ref=canvas]`, {
           style: `height:${canvasHeight}px; width:100%`,
           onmousemove: ({offsetX, offsetY}: MouseEvent) => {
-            scheduleFullRedraw();
             this.hoveredX = offsetX;
             this.hoveredY = offsetY;
             if (this.tooltipPos?.state === 'CLICK') {
@@ -309,7 +308,6 @@
             ) {
               this.tooltipPos = undefined;
             }
-            scheduleFullRedraw();
           },
           onclick: ({offsetX, offsetY}: MouseEvent) => {
             const renderNode = this.renderNodes?.find((n) =>
@@ -334,7 +332,6 @@
                 state: 'CLICK',
               };
             }
-            scheduleFullRedraw();
           },
           ondblclick: ({offsetX, offsetY}: MouseEvent) => {
             const renderNode = this.renderNodes?.find((n) =>
@@ -346,7 +343,6 @@
               return;
             }
             this.zoomRegion = renderNode?.source;
-            scheduleFullRedraw();
           },
         }),
       ),
@@ -513,7 +509,6 @@
               ...self.attrs.state,
               selectedMetricName: el.value,
             });
-            scheduleFullRedraw();
           },
         },
         attrs.metrics.map((x) => {
@@ -528,12 +523,10 @@
             value: this.rawFilterText,
             onChange: (value: string) => {
               self.rawFilterText = value;
-              scheduleFullRedraw();
             },
             onTagAdd: (tag: string) => {
               self.rawFilterText = '';
               self.attrs.onStateChange(updateState(self.attrs.state, tag));
-              scheduleFullRedraw();
             },
             onTagRemove(index: number) {
               if (index === self.attrs.state.filters.length) {
@@ -549,7 +542,6 @@
                   filters,
                 });
               }
-              scheduleFullRedraw();
             },
             onfocus() {
               self.filterFocus = true;
@@ -572,7 +564,6 @@
             ...this.attrs.state,
             view: {kind: num === 0 ? 'TOP_DOWN' : 'BOTTOM_UP'},
           });
-          scheduleFullRedraw();
         },
         disabled: this.attrs.state.view.kind === 'PIVOT',
       }),
@@ -621,7 +612,6 @@
     const filterButtonClick = (state: FlamegraphState) => {
       this.attrs.onStateChange(state);
       this.tooltipPos = undefined;
-      scheduleFullRedraw();
     };
 
     const percent = displayPercentage(
@@ -677,7 +667,6 @@
           label: 'Zoom',
           onclick: () => {
             this.zoomRegion = node.source;
-            scheduleFullRedraw();
           },
         }),
         m(Button, {
@@ -729,7 +718,7 @@
           onclick: () => {
             filterButtonClick({
               ...this.attrs.state,
-              view: {kind: 'PIVOT', pivot: name},
+              view: {kind: 'PIVOT', pivot: `^${name}$`},
             });
           },
         }),
@@ -938,35 +927,36 @@
 
 function updateState(state: FlamegraphState, filter: string): FlamegraphState {
   const lwr = filter.toLowerCase();
-  if (lwr.startsWith('ss: ') || lwr.startsWith('show stack: ')) {
+  const splitFilterFn = (f: string) => f.split(':', 2)[1].trim();
+  if (lwr.startsWith('ss:') || lwr.startsWith('show stack:')) {
     return addFilter(state, {
       kind: 'SHOW_STACK',
-      filter: filter.split(': ', 2)[1],
+      filter: splitFilterFn(filter),
     });
-  } else if (lwr.startsWith('hs: ') || lwr.startsWith('hide stack: ')) {
+  } else if (lwr.startsWith('hs:') || lwr.startsWith('hide stack:')) {
     return addFilter(state, {
       kind: 'HIDE_STACK',
-      filter: filter.split(': ', 2)[1],
+      filter: splitFilterFn(filter),
     });
-  } else if (lwr.startsWith('sff: ') || lwr.startsWith('show from frame: ')) {
+  } else if (lwr.startsWith('sff:') || lwr.startsWith('show from frame:')) {
     return addFilter(state, {
       kind: 'SHOW_FROM_FRAME',
-      filter: filter.split(': ', 2)[1],
+      filter: splitFilterFn(filter),
     });
-  } else if (lwr.startsWith('hf: ') || lwr.startsWith('hide frame: ')) {
+  } else if (lwr.startsWith('hf:') || lwr.startsWith('hide frame:')) {
     return addFilter(state, {
       kind: 'HIDE_FRAME',
-      filter: filter.split(': ', 2)[1],
+      filter: splitFilterFn(filter),
     });
-  } else if (lwr.startsWith('p:') || lwr.startsWith('pivot: ')) {
+  } else if (lwr.startsWith('p:') || lwr.startsWith('pivot:')) {
     return {
       ...state,
-      view: {kind: 'PIVOT', pivot: filter.split(': ', 2)[1]},
+      view: {kind: 'PIVOT', pivot: splitFilterFn(filter)},
     };
   }
   return addFilter(state, {
     kind: 'SHOW_STACK',
-    filter: filter,
+    filter: filter.trim(),
   });
 }
 
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index 767683e..20b9e6e 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -14,7 +14,6 @@
 
 import m from 'mithril';
 import {checkHotkey, Hotkey} from '../base/hotkeys';
-import {scheduleFullRedraw} from './raf';
 
 export interface HotkeyConfig {
   hotkey: Hotkey;
@@ -59,7 +58,7 @@
         if (checkHotkey(hotkey, e)) {
           e.preventDefault();
           callback();
-          scheduleFullRedraw('force');
+          m.redraw();
         }
       });
     }
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index c07e6fe..28d2077 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 import {defer} from '../base/deferred';
 import {Icon} from './icon';
-import {scheduleFullRedraw} from './raf';
 
 // This module deals with modal dialogs. Unlike most components, here we want to
 // render the DOM elements outside of the corresponding vdom tree. For instance
@@ -80,7 +79,7 @@
   onbeforeremove(vnode: m.VnodeDOM<ModalAttrs>) {
     const removePromise = defer<void>();
     vnode.dom.addEventListener('animationend', () => {
-      scheduleFullRedraw('force');
+      m.redraw();
       removePromise.resolve();
     });
     vnode.dom.classList.add('modal-fadeout');
@@ -234,7 +233,7 @@
 // evident why a redraw is requested.
 export function redrawModal() {
   if (currentModal !== undefined) {
-    scheduleFullRedraw('force');
+    m.redraw();
   }
 }
 
@@ -253,7 +252,7 @@
     return;
   }
   currentModal = undefined;
-  scheduleFullRedraw('force');
+  m.redraw();
 }
 
 export function getCurrentModalKey(): string | undefined {
diff --git a/ui/src/widgets/multiselect.ts b/ui/src/widgets/multiselect.ts
index a91c8ee..e6d7f43 100644
--- a/ui/src/widgets/multiselect.ts
+++ b/ui/src/widgets/multiselect.ts
@@ -18,7 +18,6 @@
 import {Checkbox} from './checkbox';
 import {EmptyState} from './empty_state';
 import {Popup, PopupPosition} from './popup';
-import {scheduleFullRedraw} from './raf';
 import {TextInput} from './text_input';
 import {Intent} from './common';
 
@@ -110,7 +109,6 @@
                       .filter(({checked}) => checked)
                       .map(({id}) => ({id, checked: false}));
                     onChange(diffs);
-                    scheduleFullRedraw();
                   },
                   disabled: !anyChecked,
                 }),
@@ -138,7 +136,6 @@
                     .filter(({checked}) => !checked)
                     .map(({id}) => ({id, checked: true}));
                   onChange(diffs);
-                  scheduleFullRedraw();
                 },
                 disabled: allChecked,
               }),
@@ -151,7 +148,6 @@
                     .filter(({checked}) => checked)
                     .map(({id}) => ({id, checked: false}));
                   onChange(diffs);
-                  scheduleFullRedraw();
                 },
                 disabled: !anyChecked,
               }),
@@ -170,7 +166,6 @@
         oninput: (event: Event) => {
           const eventTarget = event.target as HTMLTextAreaElement;
           this.searchText = eventTarget.value;
-          scheduleFullRedraw();
         },
         value: this.searchText,
         placeholder: 'Filter options...',
@@ -185,7 +180,6 @@
       return m(Button, {
         onclick: () => {
           this.searchText = '';
-          scheduleFullRedraw();
         },
         label: '',
         icon: 'close',
@@ -207,7 +201,6 @@
         className: 'pf-multiselect-item',
         onchange: () => {
           onChange([{id, checked: !checked}]);
-          scheduleFullRedraw();
         },
       });
     });
diff --git a/ui/src/widgets/popup.ts b/ui/src/widgets/popup.ts
index ac8b563..ce0b8ae 100644
--- a/ui/src/widgets/popup.ts
+++ b/ui/src/widgets/popup.ts
@@ -19,7 +19,6 @@
 import {classNames} from '../base/classnames';
 import {findRef, isOrContains, toHTMLElement} from '../base/dom_utils';
 import {assertExists} from '../base/logging';
-import {scheduleFullRedraw} from './raf';
 
 type CustomModifier = Modifier<'sameWidth', {}>;
 type ExtendedModifiers = StrictModifiers | CustomModifier;
@@ -352,13 +351,12 @@
     if (this.isOpen) {
       this.isOpen = false;
       this.onChange(this.isOpen);
-      scheduleFullRedraw('force');
+      m.redraw();
     }
   }
 
   private togglePopup() {
     this.isOpen = !this.isOpen;
     this.onChange(this.isOpen);
-    scheduleFullRedraw('force');
   }
 }
diff --git a/ui/src/widgets/table.ts b/ui/src/widgets/table.ts
index 1389907..806794a 100644
--- a/ui/src/widgets/table.ts
+++ b/ui/src/widgets/table.ts
@@ -22,7 +22,6 @@
   SortDirection,
   withDirection,
 } from '../base/comparison_utils';
-import {scheduleFullRedraw} from './raf';
 import {MenuItem, PopupMenu2} from './menu';
 import {Button} from './button';
 
@@ -147,13 +146,11 @@
     if (this._sortingInfo !== undefined) {
       this.reorder(this._sortingInfo);
     }
-    scheduleFullRedraw();
   }
 
   resetOrder() {
     this.permutation = range(this.data.length);
     this._sortingInfo = undefined;
-    scheduleFullRedraw();
   }
 
   get sortingInfo(): SortingInfo<T> | undefined {
@@ -168,7 +165,6 @@
         info.direction,
       ),
     );
-    scheduleFullRedraw();
   }
 }
 
diff --git a/ui/src/widgets/track_shell.ts b/ui/src/widgets/track_shell.ts
new file mode 100644
index 0000000..a1f082b
--- /dev/null
+++ b/ui/src/widgets/track_shell.ts
@@ -0,0 +1,434 @@
+// 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 {classNames} from '../base/classnames';
+import {DisposableStack} from '../base/disposable_stack';
+import {currentTargetOffset} from '../base/dom_utils';
+import {Bounds2D, Point2D, Vector2D} from '../base/geom';
+import {assertExists} from '../base/logging';
+import {clamp} from '../base/math_utils';
+import {hasChildren, MithrilEvent} from '../base/mithril_utils';
+import {Icons} from '../base/semantic_icons';
+import {Button, ButtonBar} from './button';
+import {Chip, ChipBar} from './chip';
+import {HTMLAttrs, Intent} from './common';
+import {MiddleEllipsis} from './middle_ellipsis';
+import {Popup} from './popup';
+
+/**
+ * This component defines the look and style of the DOM parts of a track (mainly
+ * the 'shell' part).
+ *
+ * ┌───────────────────────────────────────────────────────────────────────────┐
+ * │ pf-track                                                                  │
+ * |┌─────────────────────────────────────────────────────────────────────────┐|
+ * || pf-track__header                                                        ||
+ * │|┌─────────┐┌─────────────────────────────────────────┐┌─────────────────┐│|
+ * │|│::before ||pf-track__shell                          ││pf-track__content││|
+ * │|│(Indent) ||┌───────────────────────────────────────┐││                 ││|
+ * │|│         ||│pf-track__menubar (sticky)             │││                 ││|
+ * │|│         ||│┌───────────────┐┌────────────────────┐│││                 ││|
+ * │|│         ||││pf-track__title││pf-track__buttons   ││││                 ││|
+ * │|│         ||│└───────────────┘└────────────────────┘│││                 ││|
+ * │|│         ||└───────────────────────────────────────┘││                 ││|
+ * │|└─────────┘└─────────────────────────────────────────┘└─────────────────┘│|
+ * |└─────────────────────────────────────────────────────────────────────────┘|
+ * |┌─────────────────────────────────────────────────────────────────────────┐|
+ * || pf-track__children (if children supplied)                               ||
+ * |└─────────────────────────────────────────────────────────────────────────┘|
+ * └───────────────────────────────────────────────────────────────────────────┘
+ */
+
+export interface TrackShellAttrs extends HTMLAttrs {
+  // The title of this track.
+  readonly title: string;
+
+  // Optional subtitle to display underneath the track name.
+  readonly subtitle?: string;
+
+  // Show dropdown arrow and make clickable. Defaults to false.
+  readonly collapsible?: boolean;
+
+  // Show an up or down dropdown arrow.
+  readonly collapsed: boolean;
+
+  // Height of the track in pixels. All tracks have a fixed height.
+  readonly heightPx: number;
+
+  // Optional buttons to place on the RHS of the track shell.
+  readonly buttons?: m.Children;
+
+  // Optional list of chips to display after the track title.
+  readonly chips?: ReadonlyArray<string>;
+
+  // Render this track in error colours.
+  readonly error?: Error;
+
+  // Issues a scrollTo() on this DOM element at creation time. Default: false.
+  readonly scrollToOnCreate?: boolean;
+
+  // Style the component differently.
+  readonly summary?: boolean;
+
+  // Whether to highlight the track or not.
+  readonly highlight?: boolean;
+
+  // Whether the shell should be draggable and emit drag/drop events.
+  readonly reorderable?: boolean;
+
+  // This is the depth of the track in the tree - controls the indent level and
+  // the z-index of sticky headers.
+  readonly depth?: number;
+
+  // The stick top offset - this is the offset from the top of sticky summary
+  // track headers and sticky menu bars stick from the top of the viewport. This
+  // is used to allow nested sticky track headers and menubars of nested tracks
+  // to stick below the sticky header of their parent track(s).
+  readonly stickyTop?: number;
+
+  // The ID of the plugin that created this track.
+  readonly pluginId?: string;
+
+  // Render a lighter version of the track shell, with no buttons or chips, just
+  // the track title.
+  readonly lite?: boolean;
+
+  // Called when the track is expanded or collapsed (when the node is clicked).
+  onToggleCollapsed?(): void;
+
+  // Mouse events within the track content element.
+  onTrackContentMouseMove?(pos: Point2D, contentSize: Bounds2D): void;
+  onTrackContentMouseOut?(): void;
+  onTrackContentClick?(pos: Point2D, contentSize: Bounds2D): boolean;
+
+  // If reorderable, these functions will be called when track shells are
+  // dragged and dropped.
+  onMoveBefore?(nodeId: string): void;
+  onMoveAfter?(nodeId: string): void;
+}
+
+export class TrackShell implements m.ClassComponent<TrackShellAttrs> {
+  private mouseDownPos?: Vector2D;
+  private selectionOccurred = false;
+
+  view(vnode: m.CVnode<TrackShellAttrs>) {
+    const {attrs} = vnode;
+
+    const {
+      collapsible,
+      collapsed,
+      id,
+      summary,
+      heightPx,
+      ref,
+      depth = 0,
+      stickyTop = 0,
+      lite,
+    } = attrs;
+
+    const expanded = collapsible && !collapsed;
+    const trackHeight = heightPx;
+
+    return m(
+      '.pf-track',
+      {
+        id,
+        style: {
+          '--height': trackHeight,
+          '--depth': clamp(depth, 0, 16),
+          '--sticky-top': Math.max(0, stickyTop),
+        },
+        ref,
+      },
+      m(
+        '.pf-track__header',
+        {
+          className: classNames(
+            summary && 'pf-track__header--summary',
+            expanded && 'pf-track__header--expanded',
+            summary && expanded && 'pf-track__header--expanded--summary',
+          ),
+        },
+        this.renderShell(attrs),
+        !lite && this.renderContent(attrs),
+      ),
+      hasChildren(vnode) && m('.pf-track__children', vnode.children),
+    );
+  }
+
+  oncreate({dom, attrs}: m.VnodeDOM<TrackShellAttrs>) {
+    if (attrs.scrollToOnCreate) {
+      dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+    }
+  }
+
+  private renderShell(attrs: TrackShellAttrs): m.Children {
+    const {
+      id,
+      chips,
+      collapsible,
+      collapsed,
+      reorderable = false,
+      onMoveAfter = () => {},
+      onMoveBefore = () => {},
+      buttons,
+      highlight,
+      lite,
+    } = attrs;
+
+    const block = 'pf-track';
+    const blockElement = `${block}__shell`;
+    const dragBeforeClassName = `${blockElement}--drag-before`;
+    const dragAfterClassName = `${blockElement}--drag-after`;
+
+    return m(
+      `.pf-track__shell`,
+      {
+        className: classNames(
+          collapsible && 'pf-track__shell--clickable',
+          highlight && 'pf-track__shell--highlight',
+        ),
+        onclick: () => {
+          collapsible && attrs.onToggleCollapsed?.();
+        },
+        draggable: reorderable,
+        ondragstart: (e: DragEvent) => {
+          id && e.dataTransfer?.setData('text/plain', id);
+        },
+        ondragover: (e: DragEvent) => {
+          if (!reorderable) {
+            return;
+          }
+          const target = e.currentTarget as HTMLElement;
+          const threshold = target.offsetHeight / 2;
+          const position = currentTargetOffset(e);
+          if (position.y > threshold) {
+            target.classList.remove(dragBeforeClassName);
+            target.classList.add(dragAfterClassName);
+          } else {
+            target.classList.remove(dragAfterClassName);
+            target.classList.add(dragBeforeClassName);
+          }
+        },
+        ondragleave: (e: DragEvent) => {
+          if (!reorderable) {
+            return;
+          }
+          const target = e.currentTarget as HTMLElement;
+          const related = e.relatedTarget as HTMLElement | null;
+          if (related && !target.contains(related)) {
+            target.classList.remove(dragAfterClassName);
+            target.classList.remove(dragBeforeClassName);
+          }
+        },
+        ondrop: (e: DragEvent) => {
+          if (!reorderable) {
+            return;
+          }
+          const id = e.dataTransfer?.getData('text/plain');
+          const target = e.currentTarget as HTMLElement;
+          const threshold = target.offsetHeight / 2;
+          if (id !== undefined) {
+            const position = currentTargetOffset(e);
+            if (position.y > threshold) {
+              onMoveAfter(id);
+            } else {
+              onMoveBefore(id);
+            }
+          }
+          target.classList.remove(dragAfterClassName);
+          target.classList.remove(dragBeforeClassName);
+        },
+      },
+      lite
+        ? attrs.title
+        : m(
+            '.pf-track__menubar',
+            collapsible
+              ? m(Button, {
+                  className: 'pf-track__collapse-button',
+                  compact: true,
+                  icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp,
+                })
+              : m('.pf-track__title-spacer'),
+            m(TrackTitle, {title: attrs.title}),
+            chips &&
+              m(
+                ChipBar,
+                {className: 'pf-track__chips'},
+                chips.map((chip) =>
+                  m(Chip, {label: chip, compact: true, rounded: true}),
+                ),
+              ),
+            m(
+              ButtonBar,
+              {
+                className: 'pf-track__buttons',
+                // Block button clicks from hitting the shell's on click event
+                onclick: (e: MouseEvent) => e.stopPropagation(),
+              },
+              buttons,
+              // Always render this one last
+              attrs.error && renderCrashButton(attrs.error, attrs.pluginId),
+            ),
+            attrs.subtitle &&
+              !showSubtitleInContent(attrs) &&
+              m(
+                '.pf-track__subtitle',
+                m(MiddleEllipsis, {text: attrs.subtitle}),
+              ),
+          ),
+    );
+  }
+
+  private renderContent(attrs: TrackShellAttrs): m.Children {
+    const {
+      onTrackContentMouseMove,
+      onTrackContentMouseOut,
+      onTrackContentClick,
+      error,
+    } = attrs;
+
+    return m(
+      '.pf-track__canvas',
+      {
+        className: classNames(error && 'pf-track__canvas--error'),
+        onmousemove: (e: MithrilEvent<MouseEvent>) => {
+          e.redraw = false;
+          onTrackContentMouseMove?.(
+            currentTargetOffset(e),
+            getTargetContainerSize(e),
+          );
+        },
+        onmouseout: () => {
+          onTrackContentMouseOut?.();
+        },
+        onmousedown: (e: MouseEvent) => {
+          this.mouseDownPos = currentTargetOffset(e);
+        },
+        onmouseup: (e: MouseEvent) => {
+          if (!this.mouseDownPos) return;
+          if (
+            this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1
+          ) {
+            this.selectionOccurred = true;
+          }
+          this.mouseDownPos = undefined;
+        },
+        onclick: (e: MouseEvent) => {
+          // This click event occurs after any selection mouse up/drag events
+          // so we have to look if the mouse moved during this click to know
+          // if a selection occurred.
+          if (this.selectionOccurred) {
+            this.selectionOccurred = false;
+            return;
+          }
+
+          // Returns true if something was selected, so stop propagation.
+          if (
+            onTrackContentClick?.(
+              currentTargetOffset(e),
+              getTargetContainerSize(e),
+            )
+          ) {
+            e.stopPropagation();
+          }
+        },
+      },
+      attrs.subtitle &&
+        showSubtitleInContent(attrs) &&
+        m(MiddleEllipsis, {text: attrs.subtitle}),
+    );
+  }
+}
+
+function showSubtitleInContent(attrs: TrackShellAttrs) {
+  return attrs.summary && !attrs.collapsed;
+}
+
+function getTargetContainerSize(event: MouseEvent): Bounds2D {
+  const target = event.target as HTMLElement;
+  return target.getBoundingClientRect();
+}
+
+function renderCrashButton(error: Error, pluginId: string | undefined) {
+  return m(
+    Popup,
+    {
+      trigger: m(Button, {
+        icon: Icons.Crashed,
+        compact: true,
+      }),
+    },
+    m(
+      '.pf-track__crash-popup',
+      m('span', 'This track has crashed.'),
+      pluginId && m('span', `Owning plugin: ${pluginId}`),
+      m(Button, {
+        label: 'View & Report Crash',
+        intent: Intent.Primary,
+        className: Popup.DISMISS_POPUP_GROUP_CLASS,
+        onclick: () => {
+          throw error;
+        },
+      }),
+      // TODO(stevegolton): In the future we should provide a quick way to
+      // disable the plugin, or provide a link to the plugin page, but this
+      // relies on the plugin page being fully functional.
+    ),
+  );
+}
+
+interface TrackTitleAttrs {
+  readonly title: string;
+}
+
+class TrackTitle implements m.ClassComponent<TrackTitleAttrs> {
+  private readonly trash = new DisposableStack();
+
+  view({attrs}: m.Vnode<TrackTitleAttrs>) {
+    return m(
+      MiddleEllipsis,
+      {
+        className: 'pf-track__title',
+        text: attrs.title,
+      },
+      m('.pf-track__title-popup', attrs.title),
+    );
+  }
+
+  oncreate({dom}: m.VnodeDOM<TrackTitleAttrs>) {
+    const title = dom;
+    const popup = assertExists(dom.querySelector('.pf-track__title-popup'));
+
+    const resizeObserver = new ResizeObserver(() => {
+      // Determine whether to display a title popup based on ellipsization
+      if (popup.clientWidth > title.clientWidth) {
+        popup.classList.add('pf-track__title-popup--visible');
+      } else {
+        popup.classList.remove('pf-track__title-popup--visible');
+      }
+    });
+
+    resizeObserver.observe(title);
+    resizeObserver.observe(popup);
+
+    this.trash.defer(() => resizeObserver.disconnect());
+  }
+
+  onremove() {
+    this.trash.dispose();
+  }
+}
diff --git a/ui/src/widgets/track_widget.ts b/ui/src/widgets/track_widget.ts
deleted file mode 100644
index d9819f6..0000000
--- a/ui/src/widgets/track_widget.ts
+++ /dev/null
@@ -1,372 +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.
-
-import m from 'mithril';
-import {classNames} from '../base/classnames';
-import {DisposableStack} from '../base/disposable_stack';
-import {currentTargetOffset} from '../base/dom_utils';
-import {Bounds2D, Point2D, Vector2D} from '../base/geom';
-import {clamp} from '../base/math_utils';
-import {Icons} from '../base/semantic_icons';
-import {ButtonBar} from './button';
-import {Chip, ChipBar} from './chip';
-import {Icon} from './icon';
-import {MiddleEllipsis} from './middle_ellipsis';
-import {assertExists} from '../base/logging';
-
-/**
- * The TrackWidget defines the look and style of a track.
- *
- * ┌──────────────────────────────────────────────────────────────────┐
- * │pf-track (grid)                                                   │
- * │┌─────────────────────────────────────────┐┌─────────────────────┐│
- * ││pf-track-shell                           ││pf-track-content     ││
- * ││┌───────────────────────────────────────┐││                     ││
- * │││pf-track-menubar (sticky)              │││                     ││
- * │││┌───────────────┐┌────────────────────┐│││                     ││
- * ││││pf-track-title ││pf-track-buttons    ││││                     ││
- * │││└───────────────┘└────────────────────┘│││                     ││
- * ││└───────────────────────────────────────┘││                     ││
- * │└─────────────────────────────────────────┘└─────────────────────┘│
- * └──────────────────────────────────────────────────────────────────┘
- */
-
-export interface TrackWidgetAttrs {
-  // The title of this track.
-  readonly title: string;
-
-  // Optional subtitle to display underneath the track name.
-  readonly subtitle?: string;
-
-  // The full path to this track.
-  readonly path?: string;
-
-  // Show dropdown arrow and make clickable. Defaults to false.
-  readonly collapsible?: boolean;
-
-  // Show an up or down dropdown arrow.
-  readonly collapsed: boolean;
-
-  // Height of the track in pixels. All tracks have a fixed height.
-  readonly heightPx: number;
-
-  // Optional buttons to place on the RHS of the track shell.
-  readonly buttons?: m.Children;
-
-  // Optional list of chips to display after the track title.
-  readonly chips?: ReadonlyArray<string>;
-
-  // Render this track in error colours.
-  readonly error?: boolean;
-
-  // The integer indentation level of this track. If omitted, defaults to 0.
-  readonly indentationLevel?: number;
-
-  // Track titles are sticky. This is the offset in pixels from the top of the
-  // scrolling parent. Defaults to 0.
-  readonly topOffsetPx?: number;
-
-  // Issues a scrollTo() on this DOM element at creation time. Default: false.
-  readonly revealOnCreate?: boolean;
-
-  // Called when arrow clicked.
-  readonly onToggleCollapsed?: () => void;
-
-  // Style the component differently if it has children.
-  readonly isSummary?: boolean;
-
-  // HTML id applied to the root element.
-  readonly id: string;
-
-  // Whether to highlight the track or not.
-  readonly highlight?: boolean;
-
-  // Whether the shell should be draggable and emit drag/drop events.
-  readonly reorderable?: boolean;
-
-  // Mouse events.
-  readonly onTrackContentMouseMove?: (
-    pos: Point2D,
-    contentSize: Bounds2D,
-  ) => void;
-  readonly onTrackContentMouseOut?: () => void;
-  readonly onTrackContentClick?: (
-    pos: Point2D,
-    contentSize: Bounds2D,
-  ) => boolean;
-
-  // If reorderable, these functions will be called when track shells are
-  // dragged and dropped.
-  readonly onMoveBefore?: (nodeId: string) => void;
-  readonly onMoveAfter?: (nodeId: string) => void;
-}
-
-const TRACK_HEIGHT_MIN_PX = 18;
-const INDENTATION_LEVEL_MAX = 16;
-export class TrackWidget implements m.ClassComponent<TrackWidgetAttrs> {
-  private readonly trash = new DisposableStack();
-
-  view({attrs}: m.CVnode<TrackWidgetAttrs>) {
-    const {
-      indentationLevel = 0,
-      collapsible,
-      collapsed,
-      highlight,
-      heightPx,
-      id,
-      isSummary,
-    } = attrs;
-
-    const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
-    const expanded = collapsible && !collapsed;
-
-    return m(
-      '.pf-track',
-      {
-        id,
-        className: classNames(
-          expanded && 'pf-expanded',
-          highlight && 'pf-highlight',
-          isSummary && 'pf-is-summary',
-        ),
-        style: {
-          // Note: Sub-pixel track heights can mess with sticky elements.
-          // Round up to the nearest integer number of pixels.
-          '--indent': clamp(indentationLevel, 0, INDENTATION_LEVEL_MAX),
-          'height': `${Math.ceil(trackHeight)}px`,
-        },
-      },
-      this.renderShell(attrs),
-      this.renderContent(attrs),
-    );
-  }
-
-  oncreate({dom, attrs}: m.VnodeDOM<TrackWidgetAttrs>) {
-    if (attrs.revealOnCreate) {
-      dom.scrollIntoView({behavior: 'smooth', block: 'nearest'});
-    }
-
-    const popup = assertExists(dom.querySelector('.pf-track__title-popup'));
-    const title = assertExists(dom.querySelector('.pf-track__title'));
-
-    const resizeObserver = new ResizeObserver(() => {
-      // Work out whether to display a title popup on hover, based on whether
-      // the title is ellipsized.
-      if (popup.clientWidth > title.clientWidth) {
-        popup.classList.add('pf-visible');
-      } else {
-        popup.classList.remove('pf-visible');
-      }
-    });
-    resizeObserver.observe(title);
-    resizeObserver.observe(popup);
-    this.trash.defer(() => resizeObserver.disconnect());
-  }
-
-  onremove() {
-    this.trash.dispose();
-  }
-
-  private renderShell(attrs: TrackWidgetAttrs): m.Children {
-    const chips =
-      attrs.chips &&
-      m(
-        ChipBar,
-        attrs.chips.map((chip) =>
-          m(Chip, {label: chip, compact: true, rounded: true}),
-        ),
-      );
-
-    const {
-      id,
-      topOffsetPx = 0,
-      collapsible,
-      collapsed,
-      reorderable = false,
-      onMoveAfter = () => {},
-      onMoveBefore = () => {},
-    } = attrs;
-
-    return m(
-      `.pf-track-shell[data-track-node-id=${id}]`,
-      {
-        className: classNames(collapsible && 'pf-clickable'),
-        onclick: (e: MouseEvent) => {
-          // Block all clicks on the shell from propagating through to the
-          // canvas
-          e.stopPropagation();
-          if (collapsible) {
-            attrs.onToggleCollapsed?.();
-          }
-        },
-        draggable: reorderable,
-        ondragstart: (e: DragEvent) => {
-          e.dataTransfer?.setData('text/plain', id);
-        },
-        ondragover: (e: DragEvent) => {
-          if (!reorderable) {
-            return;
-          }
-          const target = e.currentTarget as HTMLElement;
-          const threshold = target.offsetHeight / 2;
-          if (e.offsetY > threshold) {
-            target.classList.remove('pf-drag-before');
-            target.classList.add('pf-drag-after');
-          } else {
-            target.classList.remove('pf-drag-after');
-            target.classList.add('pf-drag-before');
-          }
-        },
-        ondragleave: (e: DragEvent) => {
-          if (!reorderable) {
-            return;
-          }
-          const target = e.currentTarget as HTMLElement;
-          const related = e.relatedTarget as HTMLElement | null;
-          if (related && !target.contains(related)) {
-            target.classList.remove('pf-drag-after');
-            target.classList.remove('pf-drag-before');
-          }
-        },
-        ondrop: (e: DragEvent) => {
-          if (!reorderable) {
-            return;
-          }
-          const id = e.dataTransfer?.getData('text/plain');
-          const target = e.currentTarget as HTMLElement;
-          const threshold = target.offsetHeight / 2;
-          if (id !== undefined) {
-            if (e.offsetY > threshold) {
-              onMoveAfter(id);
-            } else {
-              onMoveBefore(id);
-            }
-          }
-          target.classList.remove('pf-drag-after');
-          target.classList.remove('pf-drag-before');
-        },
-      },
-      m(
-        '.pf-track-menubar',
-        {
-          style: {
-            position: 'sticky',
-            top: `${topOffsetPx}px`,
-          },
-        },
-        collapsible &&
-          m(Icon, {icon: collapsed ? Icons.ExpandDown : Icons.ExpandUp}),
-        m(
-          'h1.pf-track-title',
-          {
-            ref: attrs.path, // TODO(stevegolton): Replace with aria tags?
-          },
-          m(
-            MiddleEllipsis,
-            {text: attrs.title, className: 'pf-track__title'},
-            m('.pf-track__title-popup', attrs.title),
-          ),
-          chips,
-        ),
-        m(
-          ButtonBar,
-          {
-            className: 'pf-track-buttons',
-            // Block button clicks from hitting the shell's on click event
-            onclick: (e: MouseEvent) => e.stopPropagation(),
-          },
-          attrs.buttons,
-        ),
-        attrs.subtitle &&
-          !this.showSubtitleInContent(attrs) &&
-          m('h2.pf-track-subtitle', m(MiddleEllipsis, {text: attrs.subtitle})),
-      ),
-    );
-  }
-
-  private showSubtitleInContent(attrs: TrackWidgetAttrs) {
-    return attrs.isSummary && !attrs.collapsed;
-  }
-
-  private mouseDownPos?: Vector2D;
-  private selectionOccurred = false;
-
-  private renderContent(attrs: TrackWidgetAttrs): m.Children {
-    const {
-      heightPx,
-      onTrackContentMouseMove,
-      onTrackContentMouseOut,
-      onTrackContentClick,
-    } = attrs;
-    const trackHeight = Math.max(heightPx, TRACK_HEIGHT_MIN_PX);
-
-    return m(
-      '.pf-track-content',
-      {
-        style: {
-          height: `${trackHeight}px`,
-        },
-        className: classNames(attrs.error && 'pf-track-content-error'),
-        onmousemove: (e: MouseEvent) => {
-          onTrackContentMouseMove?.(
-            currentTargetOffset(e),
-            getTargetContainerSize(e),
-          );
-        },
-        onmouseout: () => {
-          onTrackContentMouseOut?.();
-        },
-        onmousedown: (e: MouseEvent) => {
-          this.mouseDownPos = currentTargetOffset(e);
-        },
-        onmouseup: (e: MouseEvent) => {
-          if (!this.mouseDownPos) return;
-          if (
-            this.mouseDownPos.sub(currentTargetOffset(e)).manhattanDistance > 1
-          ) {
-            this.selectionOccurred = true;
-          }
-          this.mouseDownPos = undefined;
-        },
-        onclick: (e: MouseEvent) => {
-          // This click event occurs after any selection mouse up/drag events
-          // so we have to look if the mouse moved during this click to know
-          // if a selection occurred.
-          if (this.selectionOccurred) {
-            this.selectionOccurred = false;
-            return;
-          }
-
-          // Returns true if something was selected, so stop propagation.
-          if (
-            onTrackContentClick?.(
-              currentTargetOffset(e),
-              getTargetContainerSize(e),
-            )
-          ) {
-            e.stopPropagation();
-          }
-        },
-      },
-      attrs.subtitle &&
-        this.showSubtitleInContent(attrs) &&
-        m('h2', m(MiddleEllipsis, {text: attrs.subtitle})),
-    );
-  }
-}
-
-function getTargetContainerSize(event: MouseEvent): Bounds2D {
-  const target = event.target as HTMLElement;
-  return target.getBoundingClientRect();
-}
diff --git a/ui/src/widgets/tree.ts b/ui/src/widgets/tree.ts
index d38bc65..5a822f8 100644
--- a/ui/src/widgets/tree.ts
+++ b/ui/src/widgets/tree.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 import {classNames} from '../base/classnames';
 import {hasChildren} from '../base/mithril_utils';
-import {scheduleFullRedraw} from './raf';
 
 // Heirachical tree layout with left and right values.
 // Right and left values of the same indentation level are horizontally aligned.
@@ -92,7 +91,6 @@
             onclick: () => {
               this.collapsed = !this.isCollapsed(vnode);
               onCollapseChanged(this.collapsed, attrs);
-              scheduleFullRedraw();
             },
           }),
           left,
@@ -215,19 +213,16 @@
             // Expanding
             if (this.renderChildren) {
               this.collapsed = false;
-              scheduleFullRedraw();
             } else {
               this.loading = true;
               fetchData().then((result) => {
                 this.loading = false;
                 this.collapsed = false;
                 this.renderChildren = result;
-                scheduleFullRedraw();
               });
             }
           }
           this.collapsed = collapsed;
-          scheduleFullRedraw();
         },
       },
       this.renderChildren && this.renderChildren(),
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 6172c94..475c360 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -14,7 +14,6 @@
 
 import {DisposableStack} from '../base/disposable_stack';
 import {Bounds2D, Rect2D} from '../base/geom';
-import {scheduleFullRedraw} from './raf';
 
 export interface VirtualScrollHelperOpts {
   overdrawPx: number;
@@ -47,7 +46,6 @@
       this._data.forEach((data) =>
         recalculatePuckRect(sliderElement, containerElement, data),
       );
-      scheduleFullRedraw('force');
     };
 
     containerElement.addEventListener('scroll', recalculateRects, {
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
index b5d8643..102828c 100644
--- a/ui/src/widgets/virtual_table.ts
+++ b/ui/src/widgets/virtual_table.ts
@@ -16,7 +16,6 @@
 import {findRef, toHTMLElement} from '../base/dom_utils';
 import {assertExists} from '../base/logging';
 import {Style} from './common';
-import {scheduleFullRedraw} from './raf';
 import {VirtualScrollHelper} from './virtual_scroll_helper';
 import {DisposableStack} from '../base/disposable_stack';
 
@@ -238,7 +237,7 @@
           const rowStart = Math.floor(rect.top / attrs.rowHeight / 2) * 2;
           const rowCount = Math.ceil(rect.height / attrs.rowHeight / 2) * 2;
           this.renderBounds = {rowStart, rowEnd: rowStart + rowCount};
-          scheduleFullRedraw();
+          m.redraw();
         },
       },
       {
@@ -248,6 +247,7 @@
           const rowStart = Math.floor(rect.top / attrs.rowHeight / 2) * 2;
           const rowEnd = Math.ceil(rect.bottom / attrs.rowHeight);
           attrs.onReload?.(rowStart, rowEnd - rowStart);
+          m.redraw();
         },
       },
     ]);